From b9a5b063e9e4a3ef9ac41dfaabf17d0e521f7c11 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Mon, 19 Jun 2023 22:31:43 +0200 Subject: [PATCH 001/215] Created Subpage and template component --- .../learning-path-management.component.html | 0 .../learning-path-management.component.ts | 7 +++++++ .../course/learning-paths/learning-paths.module.ts | 14 ++++++++++++++ .../course-management-tab-bar.component.html | 9 +++++++++ .../course-management-tab-bar.component.ts | 2 ++ .../app/course/manage/course-management.route.ts | 10 ++++++++++ 6 files changed, 42 insertions(+) create mode 100644 src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html create mode 100644 src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts create mode 100644 src/main/webapp/app/course/learning-paths/learning-paths.module.ts diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts new file mode 100644 index 000000000000..64707eb11791 --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'jhi-learning-path-management', + templateUrl: './learning-path-management.component.html', +}) +export class LearningPathManagementComponent {} diff --git a/src/main/webapp/app/course/learning-paths/learning-paths.module.ts b/src/main/webapp/app/course/learning-paths/learning-paths.module.ts new file mode 100644 index 000000000000..690dcc42db1e --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/learning-paths.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { NgxGraphModule } from '@swimlane/ngx-graph'; +import { LearningPathManagementComponent } from 'app/course/learning-paths/learning-path-management/learning-path-management.component'; + +@NgModule({ + imports: [ArtemisSharedModule, FormsModule, ReactiveFormsModule, NgxGraphModule, ArtemisSharedComponentModule, RouterModule], + declarations: [LearningPathManagementComponent], + exports: [], +}) +export class ArtemisLearningPathsModule {} diff --git a/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html b/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html index 10ab70cb7d43..2bd1a71b28b7 100644 --- a/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html +++ b/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html @@ -37,6 +37,15 @@ Competency + + + Learning Path + Date: Wed, 21 Jun 2023 11:27:39 +0200 Subject: [PATCH 002/215] Extended Management and setup service template --- .../learning-path-management.component.html | 19 +++++++ .../learning-path-management.component.ts | 52 ++++++++++++++++++- .../learningPathService.service.ts | 6 +++ src/main/webapp/app/entities/course.model.ts | 1 + src/main/webapp/i18n/de/competency.json | 5 ++ src/main/webapp/i18n/en/competency.json | 5 ++ 6 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 src/main/webapp/app/course/learning-paths/learningPathService.service.ts diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html index e69de29bb2d1..a6e4f57e1fb4 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html @@ -0,0 +1,19 @@ +
+
+ {{ 'loading' | artemisTranslate }} +
+
+ +
+
+ Disabled +
+ +
+
+
+ +
+
diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts index 64707eb11791..a5efb7bb4ebd 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts @@ -1,7 +1,55 @@ -import { Component } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { CourseManagementService } from 'app/course/manage/course-management.service'; +import { Course } from 'app/entities/course.model'; @Component({ selector: 'jhi-learning-path-management', templateUrl: './learning-path-management.component.html', }) -export class LearningPathManagementComponent {} +export class LearningPathManagementComponent implements OnInit, OnDestroy { + isLoading = false; + + courseId: number; + course: Course; + + courseSub; + + constructor(private activatedRoute: ActivatedRoute, private courseManagementService: CourseManagementService) {} + + ngOnInit(): void { + this.activatedRoute.parent!.params.subscribe((params) => { + this.courseId = +params['courseId']; + if (this.courseId) { + this.loadData(); + } + }); + } + + private loadData() { + this.isLoading = true; + + this.courseSub = this.courseManagementService.find(this.courseId).subscribe((courseResponse) => { + this.course = courseResponse.body!; + }); + + if (this.course?.learningPathEnabled) { + // TODO: load learning paths of students + } + + this.isLoading = false; + } + + /** + * On destroy unsubscribe all subscriptions. + */ + ngOnDestroy() { + if (this.courseSub) { + this.courseSub.unsubscribe(); + } + } + + enableLearningPaths() { + // TODO: Enable learning paths + } +} diff --git a/src/main/webapp/app/course/learning-paths/learningPathService.service.ts b/src/main/webapp/app/course/learning-paths/learningPathService.service.ts new file mode 100644 index 000000000000..d7b613a169e4 --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/learningPathService.service.ts @@ -0,0 +1,6 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class LearningPathService { + private resourceURL = 'api'; +} diff --git a/src/main/webapp/app/entities/course.model.ts b/src/main/webapp/app/entities/course.model.ts index 9bcff605d865..9efb8b184486 100644 --- a/src/main/webapp/app/entities/course.model.ts +++ b/src/main/webapp/app/entities/course.model.ts @@ -89,6 +89,7 @@ export class Course implements BaseEntity { public lectures?: Lecture[]; public competencies?: Competency[]; public prerequisites?: Competency[]; + public learningPathEnabled?: boolean; public exams?: Exam[]; public organizations?: Organization[]; public tutorialGroups?: TutorialGroup[]; diff --git a/src/main/webapp/i18n/de/competency.json b/src/main/webapp/i18n/de/competency.json index 5d4186cca928..9f3f6db479c3 100644 --- a/src/main/webapp/i18n/de/competency.json +++ b/src/main/webapp/i18n/de/competency.json @@ -142,6 +142,11 @@ "selfRelation": "Du kannst keine Beziehung zwischen einer Kompetenz und sich selbst erstellen.", "relationAlreadyExists": "Diese Beziehung existiert bereits." } + }, + "learningPath": { + "isDisabled": "Lernpfade sind für diesen Kurs nicht aktiviert.", + "enable": "Lernpfade aktivieren", + "enableHint": "Die Erstellung von Lernppfaden für alle Studierende kann einige Minuten dauern." } } } diff --git a/src/main/webapp/i18n/en/competency.json b/src/main/webapp/i18n/en/competency.json index e4f4a0b791d2..8ba7f919a1fb 100644 --- a/src/main/webapp/i18n/en/competency.json +++ b/src/main/webapp/i18n/en/competency.json @@ -141,6 +141,11 @@ "selfRelation": "You can not create a relation between a competency and itself.", "relationAlreadyExists": "This relation already exists." } + }, + "learningPath": { + "isDisabled": "Learning Paths are currently disabled for this course.", + "enable": "Enable Learning Paths", + "enableHint": "The creation of Learning Paths for every student may take a few minutes." } } } From cf8805c083eedf0db182add7c6b95ec8f5f288c9 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Wed, 21 Jun 2023 12:03:50 +0200 Subject: [PATCH 003/215] Refactor competencies --- src/main/java/de/tum/in/www1/artemis/domain/Course.java | 1 + src/main/java/de/tum/in/www1/artemis/domain/Exercise.java | 1 + .../java/de/tum/in/www1/artemis/domain/LearningObject.java | 2 ++ src/main/java/de/tum/in/www1/artemis/domain/User.java | 1 + .../in/www1/artemis/domain/{ => competency}/Competency.java | 5 ++++- .../artemis/domain/{ => competency}/CompetencyProgress.java | 4 +++- .../artemis/domain/{ => competency}/CompetencyRelation.java | 4 +++- .../artemis/domain/{ => competency}/CompetencyTaxonomy.java | 2 +- .../de/tum/in/www1/artemis/domain/lecture/ExerciseUnit.java | 2 +- .../de/tum/in/www1/artemis/domain/lecture/LectureUnit.java | 1 + .../artemis/repository/CompetencyProgressRepository.java | 2 +- .../artemis/repository/CompetencyRelationRepository.java | 2 +- .../tum/in/www1/artemis/repository/CompetencyRepository.java | 2 +- .../in/www1/artemis/service/CompetencyProgressService.java | 2 ++ .../de/tum/in/www1/artemis/service/CompetencyService.java | 2 ++ .../java/de/tum/in/www1/artemis/service/CourseService.java | 1 + .../de/tum/in/www1/artemis/service/LectureUnitService.java | 2 +- .../de/tum/in/www1/artemis/web/rest/CompetencyResource.java | 3 +++ .../java/de/tum/in/www1/artemis/web/rest/util/PageUtil.java | 2 +- .../artemis/assessment/ParticipantScoreIntegrationTest.java | 1 + .../in/www1/artemis/competency/CompetencyUtilService.java | 2 +- .../de/tum/in/www1/artemis/course/CourseTestService.java | 1 + .../in/www1/artemis/lecture/CompetencyIntegrationTest.java | 1 + .../de/tum/in/www1/artemis/lecture/LectureUtilService.java | 1 + 24 files changed, 36 insertions(+), 11 deletions(-) rename src/main/java/de/tum/in/www1/artemis/domain/{ => competency}/Competency.java (97%) rename src/main/java/de/tum/in/www1/artemis/domain/{ => competency}/CompetencyProgress.java (97%) rename src/main/java/de/tum/in/www1/artemis/domain/{ => competency}/CompetencyRelation.java (96%) rename src/main/java/de/tum/in/www1/artemis/domain/{ => competency}/CompetencyTaxonomy.java (96%) diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Course.java b/src/main/java/de/tum/in/www1/artemis/domain/Course.java index bdf89c895065..5b8fcfa9d033 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Course.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Course.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonView; import de.tum.in.www1.artemis.config.Constants; +import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.enumeration.CourseInformationSharingConfiguration; import de.tum.in.www1.artemis.domain.enumeration.Language; import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java b/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java index 8b1fc205cd7c..963e61df53f5 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java @@ -13,6 +13,7 @@ import com.fasterxml.jackson.annotation.*; +import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.enumeration.ExerciseType; import de.tum.in.www1.artemis.domain.enumeration.IncludedInOverallScore; import de.tum.in.www1.artemis.domain.enumeration.InitializationState; diff --git a/src/main/java/de/tum/in/www1/artemis/domain/LearningObject.java b/src/main/java/de/tum/in/www1/artemis/domain/LearningObject.java index 2198f37e3e53..6b708b9b866b 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/LearningObject.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/LearningObject.java @@ -4,6 +4,8 @@ import java.util.Optional; import java.util.Set; +import de.tum.in.www1.artemis.domain.competency.Competency; + public interface LearningObject { /** diff --git a/src/main/java/de/tum/in/www1/artemis/domain/User.java b/src/main/java/de/tum/in/www1/artemis/domain/User.java index 7096003715f1..08b11a85a983 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/User.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/User.java @@ -28,6 +28,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import de.tum.in.www1.artemis.config.Constants; +import de.tum.in.www1.artemis.domain.competency.CompetencyProgress; import de.tum.in.www1.artemis.domain.exam.ExamUser; import de.tum.in.www1.artemis.domain.lecture.LectureUnitCompletion; import de.tum.in.www1.artemis.domain.participation.Participant; diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Competency.java b/src/main/java/de/tum/in/www1/artemis/domain/competency/Competency.java similarity index 97% rename from src/main/java/de/tum/in/www1/artemis/domain/Competency.java rename to src/main/java/de/tum/in/www1/artemis/domain/competency/Competency.java index e45fca209d06..141e1ed13d97 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Competency.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/competency/Competency.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.domain; +package de.tum.in.www1.artemis.domain.competency; import java.util.HashSet; import java.util.Set; @@ -11,6 +11,9 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; +import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.DomainObject; +import de.tum.in.www1.artemis.domain.Exercise; import de.tum.in.www1.artemis.domain.lecture.ExerciseUnit; import de.tum.in.www1.artemis.domain.lecture.LectureUnit; diff --git a/src/main/java/de/tum/in/www1/artemis/domain/CompetencyProgress.java b/src/main/java/de/tum/in/www1/artemis/domain/competency/CompetencyProgress.java similarity index 97% rename from src/main/java/de/tum/in/www1/artemis/domain/CompetencyProgress.java rename to src/main/java/de/tum/in/www1/artemis/domain/competency/CompetencyProgress.java index 26e449ba3b03..f11e63c40f2f 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/CompetencyProgress.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/competency/CompetencyProgress.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.domain; +package de.tum.in.www1.artemis.domain.competency; import java.io.Serial; import java.io.Serializable; @@ -15,6 +15,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; +import de.tum.in.www1.artemis.domain.User; + /** * This class models the 'progress' association between a user and a competency. */ diff --git a/src/main/java/de/tum/in/www1/artemis/domain/CompetencyRelation.java b/src/main/java/de/tum/in/www1/artemis/domain/competency/CompetencyRelation.java similarity index 96% rename from src/main/java/de/tum/in/www1/artemis/domain/CompetencyRelation.java rename to src/main/java/de/tum/in/www1/artemis/domain/competency/CompetencyRelation.java index 4559900d80f2..f34337801ba0 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/CompetencyRelation.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/competency/CompetencyRelation.java @@ -1,7 +1,9 @@ -package de.tum.in.www1.artemis.domain; +package de.tum.in.www1.artemis.domain.competency; import javax.persistence.*; +import de.tum.in.www1.artemis.domain.DomainObject; + /** * This class models the relation between two competencies. Imagine a graph: (tail) --- type --> (head) * Because we want to keep this very generic (using the type attribute), this can not be modeled as a simple JPA relationship. diff --git a/src/main/java/de/tum/in/www1/artemis/domain/CompetencyTaxonomy.java b/src/main/java/de/tum/in/www1/artemis/domain/competency/CompetencyTaxonomy.java similarity index 96% rename from src/main/java/de/tum/in/www1/artemis/domain/CompetencyTaxonomy.java rename to src/main/java/de/tum/in/www1/artemis/domain/competency/CompetencyTaxonomy.java index 8b58573cc946..6b4b207998a4 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/CompetencyTaxonomy.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/competency/CompetencyTaxonomy.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.domain; +package de.tum.in.www1.artemis.domain.competency; import javax.persistence.AttributeConverter; import javax.persistence.Converter; diff --git a/src/main/java/de/tum/in/www1/artemis/domain/lecture/ExerciseUnit.java b/src/main/java/de/tum/in/www1/artemis/domain/lecture/ExerciseUnit.java index 486188f89012..cf25599d9e54 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/lecture/ExerciseUnit.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/lecture/ExerciseUnit.java @@ -13,8 +13,8 @@ import com.fasterxml.jackson.annotation.JsonInclude; -import de.tum.in.www1.artemis.domain.Competency; import de.tum.in.www1.artemis.domain.Exercise; +import de.tum.in.www1.artemis.domain.competency.Competency; @Entity @DiscriminatorValue("E") diff --git a/src/main/java/de/tum/in/www1/artemis/domain/lecture/LectureUnit.java b/src/main/java/de/tum/in/www1/artemis/domain/lecture/LectureUnit.java index 685578551060..d9afc184abcf 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/lecture/LectureUnit.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/lecture/LectureUnit.java @@ -13,6 +13,7 @@ import com.fasterxml.jackson.annotation.*; import de.tum.in.www1.artemis.domain.*; +import de.tum.in.www1.artemis.domain.competency.Competency; @Entity @Table(name = "lecture_unit") diff --git a/src/main/java/de/tum/in/www1/artemis/repository/CompetencyProgressRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/CompetencyProgressRepository.java index f06525a98ec7..3ab0a204049f 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/CompetencyProgressRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/CompetencyProgressRepository.java @@ -10,7 +10,7 @@ import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; -import de.tum.in.www1.artemis.domain.CompetencyProgress; +import de.tum.in.www1.artemis.domain.competency.CompetencyProgress; @Repository public interface CompetencyProgressRepository extends JpaRepository { diff --git a/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRelationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRelationRepository.java index 308e854f1922..bea1a8864da1 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRelationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRelationRepository.java @@ -7,7 +7,7 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import de.tum.in.www1.artemis.domain.CompetencyRelation; +import de.tum.in.www1.artemis.domain.competency.CompetencyRelation; /** * Spring Data JPA repository for the Competency Relation entity. diff --git a/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRepository.java index 336e63e66bd0..18be4967ccf2 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRepository.java @@ -11,7 +11,7 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import de.tum.in.www1.artemis.domain.Competency; +import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; /** diff --git a/src/main/java/de/tum/in/www1/artemis/service/CompetencyProgressService.java b/src/main/java/de/tum/in/www1/artemis/service/CompetencyProgressService.java index ff6b3f5ad841..991ea8495798 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/CompetencyProgressService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/CompetencyProgressService.java @@ -13,6 +13,8 @@ import org.springframework.stereotype.Service; import de.tum.in.www1.artemis.domain.*; +import de.tum.in.www1.artemis.domain.competency.Competency; +import de.tum.in.www1.artemis.domain.competency.CompetencyProgress; import de.tum.in.www1.artemis.domain.lecture.ExerciseUnit; import de.tum.in.www1.artemis.domain.lecture.LectureUnit; import de.tum.in.www1.artemis.domain.lecture.LectureUnitCompletion; diff --git a/src/main/java/de/tum/in/www1/artemis/service/CompetencyService.java b/src/main/java/de/tum/in/www1/artemis/service/CompetencyService.java index 0a36f15495fd..07774728b76a 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/CompetencyService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/CompetencyService.java @@ -8,6 +8,8 @@ import org.springframework.stereotype.Service; import de.tum.in.www1.artemis.domain.*; +import de.tum.in.www1.artemis.domain.competency.Competency; +import de.tum.in.www1.artemis.domain.competency.CompetencyRelation; import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.web.rest.dto.*; import de.tum.in.www1.artemis.web.rest.util.PageUtil; diff --git a/src/main/java/de/tum/in/www1/artemis/service/CourseService.java b/src/main/java/de/tum/in/www1/artemis/service/CourseService.java index cbe94d8d4858..2861309056ab 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/CourseService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/CourseService.java @@ -33,6 +33,7 @@ import de.tum.in.www1.artemis.config.Constants; import de.tum.in.www1.artemis.domain.*; +import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.enumeration.IncludedInOverallScore; import de.tum.in.www1.artemis.domain.enumeration.NotificationType; import de.tum.in.www1.artemis.domain.exam.Exam; diff --git a/src/main/java/de/tum/in/www1/artemis/service/LectureUnitService.java b/src/main/java/de/tum/in/www1/artemis/service/LectureUnitService.java index 71525a263a97..e668e74196cf 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LectureUnitService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LectureUnitService.java @@ -8,9 +8,9 @@ import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; -import de.tum.in.www1.artemis.domain.Competency; import de.tum.in.www1.artemis.domain.Lecture; import de.tum.in.www1.artemis.domain.User; +import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.lecture.ExerciseUnit; import de.tum.in.www1.artemis.domain.lecture.LectureUnit; import de.tum.in.www1.artemis.domain.lecture.LectureUnitCompletion; diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java index 5292a6eb71e8..1400be390aa7 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java @@ -15,6 +15,9 @@ import org.springframework.web.bind.annotation.*; import de.tum.in.www1.artemis.domain.*; +import de.tum.in.www1.artemis.domain.competency.Competency; +import de.tum.in.www1.artemis.domain.competency.CompetencyProgress; +import de.tum.in.www1.artemis.domain.competency.CompetencyRelation; import de.tum.in.www1.artemis.domain.lecture.ExerciseUnit; import de.tum.in.www1.artemis.domain.lecture.LectureUnit; import de.tum.in.www1.artemis.repository.*; diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/util/PageUtil.java b/src/main/java/de/tum/in/www1/artemis/web/rest/util/PageUtil.java index 5e3cbf53873e..c8b6f0706bc5 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/util/PageUtil.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/util/PageUtil.java @@ -5,10 +5,10 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; -import de.tum.in.www1.artemis.domain.Competency; import de.tum.in.www1.artemis.domain.Exercise; import de.tum.in.www1.artemis.domain.GradingScale; import de.tum.in.www1.artemis.domain.Lecture; +import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.enumeration.SortingOrder; import de.tum.in.www1.artemis.domain.exam.Exam; import de.tum.in.www1.artemis.web.rest.dto.PageableSearchDTO; diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/ParticipantScoreIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/ParticipantScoreIntegrationTest.java index 5b91bbcb9544..d6d5723dfda5 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/ParticipantScoreIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/ParticipantScoreIntegrationTest.java @@ -17,6 +17,7 @@ import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; +import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.exam.Exam; import de.tum.in.www1.artemis.domain.exam.ExamUser; import de.tum.in.www1.artemis.domain.lecture.ExerciseUnit; diff --git a/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java b/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java index ca4f3199d78c..c1c12e9df273 100644 --- a/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java @@ -3,8 +3,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import de.tum.in.www1.artemis.domain.Competency; 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; /** diff --git a/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java b/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java index e75adbfb2be7..bd35571df110 100644 --- a/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java +++ b/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java @@ -43,6 +43,7 @@ import de.tum.in.www1.artemis.competency.CompetencyUtilService; import de.tum.in.www1.artemis.config.Constants; import de.tum.in.www1.artemis.domain.*; +import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.enumeration.*; import de.tum.in.www1.artemis.domain.exam.Exam; import de.tum.in.www1.artemis.domain.exam.ExamUser; diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/CompetencyIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/CompetencyIntegrationTest.java index 6a9a606a5779..bdc05e6fd748 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/CompetencyIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/CompetencyIntegrationTest.java @@ -21,6 +21,7 @@ import de.tum.in.www1.artemis.competency.CompetencyUtilService; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; +import de.tum.in.www1.artemis.domain.competency.*; import de.tum.in.www1.artemis.domain.enumeration.ExerciseMode; import de.tum.in.www1.artemis.domain.enumeration.SubmissionType; import de.tum.in.www1.artemis.domain.lecture.ExerciseUnit; diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LectureUtilService.java b/src/test/java/de/tum/in/www1/artemis/lecture/LectureUtilService.java index 92c450b8b6c2..770de389c202 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LectureUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LectureUtilService.java @@ -9,6 +9,7 @@ import de.tum.in.www1.artemis.course.CourseFactory; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; +import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.lecture.*; import de.tum.in.www1.artemis.repository.*; From 1c6752418280e4d87414e52d84ecf1acc96d58b4 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Wed, 21 Jun 2023 12:52:15 +0200 Subject: [PATCH 004/215] Fix translations --- .../learning-path-management.component.html | 13 ++++++++++--- .../learning-path-management.component.ts | 3 ++- .../course-management-tab-bar.component.html | 2 +- src/main/webapp/i18n/de/competency.json | 10 +++++++--- src/main/webapp/i18n/en/competency.json | 10 +++++++--- 5 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html index a6e4f57e1fb4..7f1c73d0bb67 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html @@ -5,10 +5,17 @@
+

Learning Path Management

- Disabled -
-
diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts index a5efb7bb4ebd..dd41b193739d 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts @@ -2,6 +2,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { CourseManagementService } from 'app/course/manage/course-management.service'; import { Course } from 'app/entities/course.model'; +import { Subscription } from 'rxjs'; @Component({ selector: 'jhi-learning-path-management', @@ -13,7 +14,7 @@ export class LearningPathManagementComponent implements OnInit, OnDestroy { courseId: number; course: Course; - courseSub; + courseSub: Subscription; constructor(private activatedRoute: ActivatedRoute, private courseManagementService: CourseManagementService) {} diff --git a/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html b/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html index 573006b3545e..dce19c655021 100644 --- a/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html +++ b/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html @@ -44,7 +44,7 @@ routerLinkActive="active" > - Learning Path + Learning Paths
Date: Wed, 21 Jun 2023 13:26:05 +0200 Subject: [PATCH 005/215] Update learningPathService.service.ts --- .../learning-paths/learningPathService.service.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/webapp/app/course/learning-paths/learningPathService.service.ts b/src/main/webapp/app/course/learning-paths/learningPathService.service.ts index d7b613a169e4..fa64567c62ee 100644 --- a/src/main/webapp/app/course/learning-paths/learningPathService.service.ts +++ b/src/main/webapp/app/course/learning-paths/learningPathService.service.ts @@ -1,6 +1,16 @@ import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Course } from 'app/entities/course.model'; @Injectable({ providedIn: 'root' }) export class LearningPathService { private resourceURL = 'api'; + + constructor(private httpClient: HttpClient) {} + + enableLearningPaths(courseId: number): Observable> { + // return this.httpClient.put(`${this.resourceURL}/courses/${courseId}/learning-paths/enable`, { observe: 'response' }) + return new Observable(); + } } From 080b57c54e541188efd5941f2344568f80b2eb82 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Wed, 21 Jun 2023 13:26:13 +0200 Subject: [PATCH 006/215] Update learning-path-management.component.ts --- .../learning-path-management.component.ts | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts index dd41b193739d..7a776e137c67 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts @@ -3,6 +3,11 @@ import { ActivatedRoute } from '@angular/router'; import { CourseManagementService } from 'app/course/manage/course-management.service'; import { Course } from 'app/entities/course.model'; import { Subscription } from 'rxjs'; +import { LearningPathService } from 'app/course/learning-paths/learningPathService.service'; +import { finalize } from 'rxjs/operators'; +import { HttpErrorResponse } from '@angular/common/http'; +import { onError } from 'app/shared/util/global.utils'; +import { AlertService } from 'app/core/util/alert.service'; @Component({ selector: 'jhi-learning-path-management', @@ -16,7 +21,12 @@ export class LearningPathManagementComponent implements OnInit, OnDestroy { courseSub: Subscription; - constructor(private activatedRoute: ActivatedRoute, private courseManagementService: CourseManagementService) {} + constructor( + private activatedRoute: ActivatedRoute, + private courseManagementService: CourseManagementService, + private learningPathService: LearningPathService, + private alertService: AlertService, + ) {} ngOnInit(): void { this.activatedRoute.parent!.params.subscribe((params) => { @@ -51,6 +61,16 @@ export class LearningPathManagementComponent implements OnInit, OnDestroy { } enableLearningPaths() { - // TODO: Enable learning paths + this.isLoading = true; + this.learningPathService + .enableLearningPaths(this.courseId) + .pipe( + finalize(() => { + this.isLoading = false; + }), + ) + .subscribe({ + error: (res: HttpErrorResponse) => onError(this.alertService, res), + }); } } From 37f1ff89f71e6f444f7eeea270b926413ee503fd Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Wed, 21 Jun 2023 14:12:58 +0200 Subject: [PATCH 007/215] Fix teamscale finding --- .../in/www1/artemis/lecture/CompetencyIntegrationTest.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/CompetencyIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/CompetencyIntegrationTest.java index bdc05e6fd748..16bf8312687f 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/CompetencyIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/CompetencyIntegrationTest.java @@ -21,7 +21,10 @@ import de.tum.in.www1.artemis.competency.CompetencyUtilService; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; -import de.tum.in.www1.artemis.domain.competency.*; +import de.tum.in.www1.artemis.domain.competency.Competency; +import de.tum.in.www1.artemis.domain.competency.CompetencyProgress; +import de.tum.in.www1.artemis.domain.competency.CompetencyRelation; +import de.tum.in.www1.artemis.domain.competency.CompetencyTaxonomy; import de.tum.in.www1.artemis.domain.enumeration.ExerciseMode; import de.tum.in.www1.artemis.domain.enumeration.SubmissionType; import de.tum.in.www1.artemis.domain.lecture.ExerciseUnit; From 0467ecf4919b4a1cdbfa13a4be0e1e27651050fc Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 22 Jun 2023 18:06:00 +0200 Subject: [PATCH 008/215] First draft of server side --- .../de/tum/in/www1/artemis/domain/Course.java | 25 +++ .../de/tum/in/www1/artemis/domain/User.java | 13 ++ .../artemis/domain/competency/Competency.java | 12 ++ .../domain/competency/LearningPath.java | 101 ++++++++++ .../repository/LearningPathRepository.java | 33 ++++ .../www1/artemis/service/CourseService.java | 8 +- .../artemis/service/LearningPathService.java | 76 ++++++++ .../web/rest/LearningPathResource.java | 93 ++++++++++ .../www1/artemis/web/rest/util/PageUtil.java | 8 + .../competency/CompetencyUtilService.java | 16 +- .../artemis/exercise/ExerciseUtilService.java | 7 + .../lecture/LearningPathIntegrationTest.java | 172 ++++++++++++++++++ 12 files changed, 561 insertions(+), 3 deletions(-) create mode 100644 src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java create mode 100644 src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java create mode 100644 src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java create mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java create mode 100644 src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Course.java b/src/main/java/de/tum/in/www1/artemis/domain/Course.java index 5b8fcfa9d033..5fc1c954150b 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Course.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Course.java @@ -20,6 +20,7 @@ import de.tum.in.www1.artemis.config.Constants; import de.tum.in.www1.artemis.domain.competency.Competency; +import de.tum.in.www1.artemis.domain.competency.LearningPath; import de.tum.in.www1.artemis.domain.enumeration.CourseInformationSharingConfiguration; import de.tum.in.www1.artemis.domain.enumeration.Language; import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; @@ -205,6 +206,14 @@ public class Course extends DomainObject { @OrderBy("title") private Set competencies = new HashSet<>(); + @Column(name = "learning_paths_enabled", nullable = false) + private boolean learningPathsEnabled = false; + + @OneToMany(mappedBy = "course", cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.LAZY) + @JsonIgnoreProperties("course") + @OrderBy("id") + private Set learningPaths = new HashSet<>(); + @OneToMany(mappedBy = "course", cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.LAZY) @JsonIgnoreProperties(value = "course", allowSetters = true) @OrderBy("title") @@ -720,6 +729,22 @@ public void setCompetencies(Set competencies) { this.competencies = competencies; } + public boolean getLearningPathsEnabled() { + return learningPathsEnabled; + } + + public void setLeanringPathsEnabled(boolean learningPathsEnabled) { + this.learningPathsEnabled = learningPathsEnabled; + } + + public Set getLearningPaths() { + return learningPaths; + } + + public void setLearningPaths(Set learningPaths) { + this.learningPaths = learningPaths; + } + public boolean hasCourseArchive() { return courseArchivePath != null && !courseArchivePath.isEmpty(); } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/User.java b/src/main/java/de/tum/in/www1/artemis/domain/User.java index 08b11a85a983..01e9a397a7e7 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/User.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/User.java @@ -29,6 +29,7 @@ import de.tum.in.www1.artemis.config.Constants; import de.tum.in.www1.artemis.domain.competency.CompetencyProgress; +import de.tum.in.www1.artemis.domain.competency.LearningPath; import de.tum.in.www1.artemis.domain.exam.ExamUser; import de.tum.in.www1.artemis.domain.lecture.LectureUnitCompletion; import de.tum.in.www1.artemis.domain.participation.Participant; @@ -167,6 +168,10 @@ public class User extends AbstractAuditingEntity implements Participant { @JsonIgnore private Set competencyProgresses = new HashSet<>(); + @OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE, orphanRemoval = true) + @JsonIgnore + private Set learningPaths = new HashSet<>(); + @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.LAZY) @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) @JsonIgnore @@ -361,6 +366,14 @@ public void setCompetencyProgresses(Set competencyProgresses this.competencyProgresses = competencyProgresses; } + public Set getLearningPaths() { + return learningPaths; + } + + public void setLearningPaths(Set learningPaths) { + this.learningPaths = learningPaths; + } + public Set getExamUsers() { return examUsers; } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/competency/Competency.java b/src/main/java/de/tum/in/www1/artemis/domain/competency/Competency.java index 141e1ed13d97..3cf8d50db804 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/competency/Competency.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/competency/Competency.java @@ -67,6 +67,10 @@ public class Competency extends DomainObject { @JsonIgnoreProperties({ "user", "competency" }) private Set userProgress = new HashSet<>(); + @ManyToMany(mappedBy = "competencies") + @JsonIgnoreProperties({ "competencies", "course" }) + private Set learningPaths = new HashSet<>(); + public String getTitle() { return title; } @@ -179,6 +183,14 @@ public void setUserProgress(Set userProgress) { this.userProgress = userProgress; } + public Set getLearningPaths() { + return learningPaths; + } + + public void setLearningPaths(Set learningPaths) { + this.learningPaths = learningPaths; + } + /** * Ensure that exercise units are connected to competencies through the corresponding exercise */ diff --git a/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java b/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java new file mode 100644 index 000000000000..68631fc59d83 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java @@ -0,0 +1,101 @@ +package de.tum.in.www1.artemis.domain.competency; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import javax.persistence.*; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonView; + +import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.DomainObject; +import de.tum.in.www1.artemis.domain.User; +import de.tum.in.www1.artemis.domain.view.QuizView; + +@Entity +@Table(name = "learning_path") +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) +public class LearningPath extends DomainObject { + + @ManyToOne + @JsonIgnore + private User user; + + @ManyToOne + @JoinColumn(name = "course_id") + @JsonIgnoreProperties({ "competencies", "prerequisites" }) + private Course course; + + @ManyToMany + @JoinTable(name = "competency_learning_path", joinColumns = @JoinColumn(name = "learning_path_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "competency_id", referencedColumnName = "id")) + @JsonIgnoreProperties({ "exercises", "course", "learningPaths" }) + @JsonView(QuizView.Before.class) + private Set competencies = new HashSet<>(); + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public Course getCourse() { + return course; + } + + public void setCourse(Course course) { + this.course = course; + } + + public Set getCompetencies() { + return competencies; + } + + public void setCompetencies(Set competencies) { + this.competencies = competencies; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + LearningPath that = (LearningPath) obj; + return getId().equals(that.getId()); + } + + @Override + public int hashCode() { + return Objects.hashCode(getId()); + } + + @Override + public String toString() { + return "LearningPath{" + "id=" + getId() + ", user=" + user + ", course=" + course + ", competencies=" + competencies + '}'; + } + + public enum LearningPathSearchColumn { + + ID("id"), STUDENT_LOGIN("student.login"); + + private final String mappedColumnName; + + LearningPathSearchColumn(String mappedColumnName) { + this.mappedColumnName = mappedColumnName; + } + + public String getMappedColumnName() { + return mappedColumnName; + } + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java new file mode 100644 index 000000000000..95c110d2e821 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java @@ -0,0 +1,33 @@ +package de.tum.in.www1.artemis.repository; + +import java.util.List; +import java.util.Set; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +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.competency.LearningPath; + +@Repository +public interface LearningPathRepository extends JpaRepository { + + List findByCourseIdAndUserId(long courseId, long userId); + + @Query(""" + SELECT lp + FROM LearningPath lp + WHERE lp.course.id = :courseId + """) + Set findAllForCourse(@Param("courseId") long courseId); + + @Query(""" + SELECT lp + FROM LearningPath lp + WHERE (lp.course.id = :courseId) AND (lp.user.login LIKE %:partialLogin%) + """) + Page findByLoginInCourse(@Param("partialLogin") String partialLogin, @Param("courseId") long courseId, Pageable pageable); +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/CourseService.java b/src/main/java/de/tum/in/www1/artemis/service/CourseService.java index 2861309056ab..f72ca384b67f 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/CourseService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/CourseService.java @@ -149,6 +149,8 @@ public class CourseService { private final ConversationRepository conversationRepository; + private final LearningPathService learningPathService; + public CourseService(Environment env, ArtemisAuthenticationProvider artemisAuthenticationProvider, CourseRepository courseRepository, ExerciseService exerciseService, ExerciseDeletionService exerciseDeletionService, AuthorizationCheckService authCheckService, UserRepository userRepository, LectureService lectureService, GroupNotificationRepository groupNotificationRepository, ExerciseGroupRepository exerciseGroupRepository, AuditEventRepository auditEventRepository, @@ -159,7 +161,7 @@ public CourseService(Environment env, ArtemisAuthenticationProvider artemisAuthe ComplaintResponseRepository complaintResponseRepository, SubmissionRepository submissionRepository, ProgrammingExerciseRepository programmingExerciseRepository, ExerciseRepository exerciseRepository, ParticipantScoreRepository participantScoreRepository, PresentationPointsCalculationService presentationPointsCalculationService, TutorialGroupRepository tutorialGroupRepository, TutorialGroupService tutorialGroupService, TutorialGroupsConfigurationRepository tutorialGroupsConfigurationRepository, - PlagiarismCaseRepository plagiarismCaseRepository, ConversationRepository conversationRepository) { + PlagiarismCaseRepository plagiarismCaseRepository, ConversationRepository conversationRepository, LearningPathService learningPathService) { this.env = env; this.artemisAuthenticationProvider = artemisAuthenticationProvider; this.courseRepository = courseRepository; @@ -197,6 +199,7 @@ public CourseService(Environment env, ArtemisAuthenticationProvider artemisAuthe this.tutorialGroupsConfigurationRepository = tutorialGroupsConfigurationRepository; this.plagiarismCaseRepository = plagiarismCaseRepository; this.conversationRepository = conversationRepository; + this.learningPathService = learningPathService; } /** @@ -470,6 +473,9 @@ public Course retrieveCourseOverExerciseGroupOrCourseId(Exercise exercise) { public void enrollUserForCourseOrThrow(User user, Course course) { authCheckService.checkUserAllowedToEnrollInCourseElseThrow(user, course); userService.addUserToGroup(user, course.getStudentGroupName(), Role.STUDENT); + if (course.getLearningPathsEnabled()) { + learningPathService.generateLearningPathForUser(course, user); + } final var auditEvent = new AuditEvent(user.getLogin(), Constants.ENROLL_IN_COURSE, "course=" + course.getTitle()); auditEventRepository.add(auditEvent); log.info("User {} has successfully enrolled in course {}", user.getLogin(), course.getTitle()); diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java new file mode 100644 index 000000000000..8d304b26197a --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -0,0 +1,76 @@ +package de.tum.in.www1.artemis.service; + +import javax.validation.constraints.NotNull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Service; + +import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.User; +import de.tum.in.www1.artemis.domain.competency.LearningPath; +import de.tum.in.www1.artemis.repository.LearningPathRepository; +import de.tum.in.www1.artemis.repository.UserRepository; +import de.tum.in.www1.artemis.web.rest.dto.PageableSearchDTO; +import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; +import de.tum.in.www1.artemis.web.rest.util.PageUtil; + +@Service +public class LearningPathService { + + private final Logger log = LoggerFactory.getLogger(LearningPathService.class); + + private final UserRepository userRepository; + + private final LearningPathRepository learningPathRepository; + + public LearningPathService(UserRepository userRepository, LearningPathRepository learningPathRepository) { + this.userRepository = userRepository; + this.learningPathRepository = learningPathRepository; + } + + /** + * Generate learning paths for all students enrolled in the course + * + * @param course course the learning paths are created for + */ + public void generateLearningPaths(@NotNull Course course) { + var students = userRepository.getStudents(course); + students.stream().parallel().forEach((student) -> { + generateLearningPathForUser(course, student); + }); + log.debug("Successfully created learning paths for all {} students in course (id={})", students.size(), course.getId()); + } + + /** + * Generate learning path for the user in the course + * + * @param course course that defines the learning path + * @param user student for which the learning path is generated + */ + public void generateLearningPathForUser(@NotNull Course course, @NotNull User user) { + var existingLearningPaths = learningPathRepository.findByCourseIdAndUserId(course.getId(), user.getId()); + if (existingLearningPaths == null || existingLearningPaths.isEmpty()) { + LearningPath lpToCreate = new LearningPath(); + lpToCreate.setUser(user); + lpToCreate.setCourse(course); + lpToCreate.setCompetencies(course.getCompetencies()); + var persistedLearningPath = learningPathRepository.save(lpToCreate); + log.debug("Created LearningPath (id={}) for user (id={}) in course (id={})", persistedLearningPath.getId(), user.getId(), course.getId()); + } + } + + /** + * Search for all learning paths fitting a {@link PageableSearchDTO search query}. The result is paged. + * + * @param search The search query defining the search term and the size of the returned page + * @return A wrapper object containing a list of all found learning paths and the total number of pages + */ + public SearchResultPageDTO getAllOfCourseOnPageWithSize(final PageableSearchDTO search, final Course course) { + final var pageable = PageUtil.createLearningPathPageRequest(search); + final var searchTerm = search.getSearchTerm(); + final Page learningPathPage = learningPathRepository.findByLoginInCourse(searchTerm, course.getId(), pageable); + return new SearchResultPageDTO<>(learningPathPage.getContent(), learningPathPage.getTotalPages()); + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java new file mode 100644 index 000000000000..b17e5273e5d9 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java @@ -0,0 +1,93 @@ +package de.tum.in.www1.artemis.web.rest; + +import javax.ws.rs.BadRequestException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.User; +import de.tum.in.www1.artemis.domain.competency.LearningPath; +import de.tum.in.www1.artemis.repository.*; +import de.tum.in.www1.artemis.security.Role; +import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor; +import de.tum.in.www1.artemis.service.AuthorizationCheckService; +import de.tum.in.www1.artemis.service.LearningPathService; +import de.tum.in.www1.artemis.web.rest.dto.PageableSearchDTO; +import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; + +@RestController +@RequestMapping("/api") +public class LearningPathResource { + + private final Logger log = LoggerFactory.getLogger(LearningPathResource.class); + + private final CourseRepository courseRepository; + + private final AuthorizationCheckService authorizationCheckService; + + private final UserRepository userRepository; + + private final CompetencyRepository competencyRepository; + + private final LearningPathRepository learningPathRepository; + + private final LearningPathService learningPathService; + + public LearningPathResource(CourseRepository courseRepository, AuthorizationCheckService authorizationCheckService, UserRepository userRepository, + CompetencyRepository competencyRepository, LearningPathService learningPathService, LearningPathRepository learningPathRepository) { + this.courseRepository = courseRepository; + this.authorizationCheckService = authorizationCheckService; + this.userRepository = userRepository; + this.competencyRepository = competencyRepository; + this.learningPathService = learningPathService; + this.learningPathRepository = learningPathRepository; + } + + /** + * PUT /courses/:courseId/learning-paths/enable : enables and generates learning paths for the course + * + * @param courseId the id of the course for which the learning paths should be enabled + * @return the ResponseEntity with status 200 (OK) and with body the updated course + */ + @PutMapping("/courses/{courseId}/learning-paths/enable") + @EnforceAtLeastInstructor + public ResponseEntity enableLearningPathsForCourse(@PathVariable Long courseId) { + log.debug("REST request to enable learning paths for course with id: {}", courseId); + Course course = courseRepository.findByIdElseThrow(courseId); + User user = userRepository.getUserWithGroupsAndAuthorities(); + authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, user); + if (course.getLearningPathsEnabled()) { + throw new BadRequestException("Learning paths are already enabled for this course."); + } + + learningPathService.generateLearningPaths(course); + + course.setLeanringPathsEnabled(true); + course = courseRepository.save(course); + + return ResponseEntity.ok(course); + } + + /** + * GET /course/:courseId/learning-paths : gets all the learning paths of a course + * + * @param courseId the id of the course for which the learning paths should be fetched + * @return the ResponseEntity with status 200 (OK) and with body the desired page, sorted and matching the given query + */ + @GetMapping("/course/{courseId}/learning-paths") + @EnforceAtLeastInstructor + public ResponseEntity> getLearningPathsOnPage(@PathVariable Long courseId, PageableSearchDTO search) { + log.debug("REST request to get learning paths for course with id: {}", courseId); + Course course = courseRepository.findByIdElseThrow(courseId); + User user = userRepository.getUserWithGroupsAndAuthorities(); + authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, user); + if (!course.getLearningPathsEnabled()) { + throw new BadRequestException("Learning paths are not enabled for this course."); + } + + return ResponseEntity.ok(learningPathService.getAllOfCourseOnPageWithSize(search, course)); + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/util/PageUtil.java b/src/main/java/de/tum/in/www1/artemis/web/rest/util/PageUtil.java index c8b6f0706bc5..cae1a8ecb76b 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/util/PageUtil.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/util/PageUtil.java @@ -9,6 +9,7 @@ import de.tum.in.www1.artemis.domain.GradingScale; import de.tum.in.www1.artemis.domain.Lecture; import de.tum.in.www1.artemis.domain.competency.Competency; +import de.tum.in.www1.artemis.domain.competency.LearningPath; import de.tum.in.www1.artemis.domain.enumeration.SortingOrder; import de.tum.in.www1.artemis.domain.exam.Exam; import de.tum.in.www1.artemis.web.rest.dto.PageableSearchDTO; @@ -36,6 +37,13 @@ public static PageRequest createCompetencyPageRequest(PageableSearchDTO return PageRequest.of(search.getPage() - 1, search.getPageSize(), sortOptions); } + @NotNull + public static PageRequest createLearningPathPageRequest(PageableSearchDTO search) { + var sortOptions = Sort.by(LearningPath.LearningPathSearchColumn.valueOf(search.getSortedColumn()).getMappedColumnName()); + sortOptions = search.getSortingOrder() == SortingOrder.ASCENDING ? sortOptions.ascending() : sortOptions.descending(); + return PageRequest.of(search.getPage() - 1, search.getPageSize(), sortOptions); + } + @NotNull public static PageRequest createExamPageRequest(PageableSearchDTO search) { var sortOptions = Sort.by(Exam.ExamSearchColumn.valueOf(search.getSortedColumn()).getMappedColumnName()); diff --git a/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java b/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java index c1c12e9df273..c78015566a40 100644 --- a/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java @@ -16,11 +16,23 @@ public class CompetencyUtilService { @Autowired private CompetencyRepository competencyRepo; - public Competency createCompetency(Course course) { + private Competency createCompetency(Course course, String suffix) { Competency competency = new Competency(); - competency.setTitle("Example Competency"); + competency.setTitle("Example Competency" + suffix); competency.setDescription("Magna pars studiorum, prodita quaerimus."); competency.setCourse(course); return competencyRepo.save(competency); } + + public Competency createCompetency(Course course) { + return createCompetency(course, ""); + } + + public Competency[] createCompetencies(Course course, int numberOfCompetencies) { + Competency[] competencies = new Competency[numberOfCompetencies]; + for (int i = 0; i < competencies.length; i++) { + competencies[i] = createCompetency(course, "" + i); + } + return competencies; + } } diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/ExerciseUtilService.java b/src/test/java/de/tum/in/www1/artemis/exercise/ExerciseUtilService.java index bf6a9835debf..cb55cb57feaa 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/ExerciseUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/ExerciseUtilService.java @@ -12,6 +12,7 @@ import org.springframework.stereotype.Service; import de.tum.in.www1.artemis.domain.*; +import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; import de.tum.in.www1.artemis.domain.enumeration.IncludedInOverallScore; import de.tum.in.www1.artemis.domain.exam.Exam; @@ -305,4 +306,10 @@ public ProgrammingExercise findProgrammingExerciseWithTitle(Collection // just to prevent compiler warnings, we have failed anyway here return new ProgrammingExercise(); } + + public Exercise addCompetenciesToExercise(Exercise exercise, Set competencies) { + exercise = exerciseRepo.findByIdElseThrow(exercise.getId()); + exercise.setCompetencies(competencies); + return exerciseRepo.save(exercise); + } } diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java new file mode 100644 index 000000000000..c9ad61881bc2 --- /dev/null +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java @@ -0,0 +1,172 @@ +package de.tum.in.www1.artemis.lecture; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.ZonedDateTime; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.test.context.support.WithMockUser; + +import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.competency.CompetencyUtilService; +import de.tum.in.www1.artemis.course.CourseUtilService; +import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.User; +import de.tum.in.www1.artemis.domain.competency.Competency; +import de.tum.in.www1.artemis.domain.competency.LearningPath; +import de.tum.in.www1.artemis.repository.CourseRepository; +import de.tum.in.www1.artemis.service.LearningPathService; +import de.tum.in.www1.artemis.user.UserUtilService; +import de.tum.in.www1.artemis.util.PageableSearchUtilService; + +public class LearningPathIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { + + private static final String TEST_PREFIX = "learningpathintegration"; + + @Autowired + private CourseRepository courseRepository; + + @Autowired + private LearningPathService learningPathService; + + @Autowired + private UserUtilService userUtilService; + + @Autowired + private CourseUtilService courseUtilService; + + @Autowired + CompetencyUtilService competencyUtilService; + + @Autowired + PageableSearchUtilService pageableSearchUtilService; + + private Course course; + + private Competency[] competencies; + + private final int NUMBER_OF_STUDENTS = 5; + + private final String STUDENT_OF_COURSE = TEST_PREFIX + "student1"; + + private final String TUTOR_OF_COURSE = TEST_PREFIX + "tutor1"; + + private final String EDITOR_OF_COURSE = TEST_PREFIX + "editor1"; + + private final String INSTRUCTOR_OF_COURSE = TEST_PREFIX + "instructor1"; + + @BeforeEach + void setupTestScenario() { + participantScoreScheduleService.activate(); + + userUtilService.addUsers(TEST_PREFIX, NUMBER_OF_STUDENTS, 1, 1, 1); + + // Add users that are not in the course + userUtilService.createAndSaveUser(TEST_PREFIX + "student1337"); + userUtilService.createAndSaveUser(TEST_PREFIX + "instructor1337"); + + // creating course + course = courseUtilService.createCourse(); + + competencies = competencyUtilService.createCompetencies(course, 5); + + } + + private void enableLearningPathsForTestingCourse() { + course.setLeanringPathsEnabled(true); + learningPathService.generateLearningPaths(course); + course = courseRepository.save(course); + } + + private ZonedDateTime now() { + return ZonedDateTime.now(); + } + + private ZonedDateTime past(long days) { + return now().minusDays(days); + } + + private ZonedDateTime future(long days) { + return now().plusDays(days); + } + + private void testAllPreAuthorize() throws Exception { + request.putWithResponseBody("/courses/" + course.getId() + "/learning-paths/enable", null, Course.class, HttpStatus.FORBIDDEN); + final var search = pageableSearchUtilService.configureSearch(""); + request.getSearchResult("/courses/" + course.getId() + "/learning-paths", HttpStatus.FORBIDDEN, LearningPath.class, pageableSearchUtilService.searchMapping(search)); + } + + @Test + @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") + void testAll_asStudent() throws Exception { + this.testAllPreAuthorize(); + } + + @Test + @WithMockUser(username = TUTOR_OF_COURSE, roles = "TA") + void testAll_asTutor() throws Exception { + this.testAllPreAuthorize(); + } + + @Test + @WithMockUser(username = EDITOR_OF_COURSE, roles = "EDITOR") + void testAll_asEditor() throws Exception { + this.testAllPreAuthorize(); + } + + @Test + @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") + void testEnableLearningPaths() throws Exception { + request.putWithResponseBody("/courses/" + course.getId() + "/learning-paths/enable", course, Course.class, HttpStatus.OK); + final var updatedCourse = courseRepository.findByIdElseThrow(course.getId()); + assertThat(updatedCourse.getLearningPathsEnabled()).isTrue().as("should enable LearningPaths"); + assertThat(updatedCourse.getLearningPaths()).isNotNull(); + assertThat(updatedCourse.getLearningPaths().size()).isEqualTo(NUMBER_OF_STUDENTS).as("should create LearningPath for each student"); + } + + @Test + @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") + void testEnableLearningPathsAlreadyEnabled() throws Exception { + course.setLeanringPathsEnabled(true); + courseRepository.save(course); + request.putWithResponseBody("/courses/" + course.getId() + "/learning-paths/enable", course, Course.class, HttpStatus.BAD_REQUEST); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1337", roles = "USER") + void testGenerateLearningPathOnEnrollment() throws Exception { + final var updatedStudent = request.postWithResponseBody("/api/courses/" + course.getId() + "/enroll", null, User.class, HttpStatus.OK); + assertThat(updatedStudent.getLearningPaths()).isNotNull(); + assertThat(updatedStudent.getLearningPaths().size()).isEqualTo(1).as("should create LearningPath for student"); + } + + @Test + @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") + void testGetLearningPathsOnPageForCourseLearningPathsDisabled() throws Exception { + final var search = pageableSearchUtilService.configureSearch(""); + request.getSearchResult("/courses/" + course.getId() + "/learning-paths", HttpStatus.BAD_REQUEST, LearningPath.class, pageableSearchUtilService.searchMapping(search)); + } + + @Test + @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") + void testGetLearningPathsOnPageForCourseEmpty() throws Exception { + enableLearningPathsForTestingCourse(); + final var search = pageableSearchUtilService.configureSearch(STUDENT_OF_COURSE + "SuffixThatAllowsTheResultToBeEmpty"); + final var result = request.getSearchResult("/courses/" + course.getId() + "/learning-paths", HttpStatus.OK, LearningPath.class, + pageableSearchUtilService.searchMapping(search)); + assertThat(result.getResultsOnPage()).isNullOrEmpty(); + } + + @Test + @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") + void testGetLearningPathsOnPageForCourseExactlyStudent() throws Exception { + enableLearningPathsForTestingCourse(); + final var search = pageableSearchUtilService.configureSearch(STUDENT_OF_COURSE); + final var result = request.getSearchResult("/courses/" + course.getId() + "/learning-paths", HttpStatus.OK, LearningPath.class, + pageableSearchUtilService.searchMapping(search)); + assertThat(result.getResultsOnPage()).hasSize(1); + } +} From 8adca84a7c2d7f14083de357c425ad786b179fce Mon Sep 17 00:00:00 2001 From: MaximilianAnzinger Date: Thu, 22 Jun 2023 21:36:38 +0200 Subject: [PATCH 009/215] Add liquibase changelog --- .../domain/competency/LearningPath.java | 1 + .../changelog/20230622000000_changelog.xml | 27 +++++++++++++++++++ .../resources/config/liquibase/master.xml | 1 + 3 files changed, 29 insertions(+) create mode 100644 src/main/resources/config/liquibase/changelog/20230622000000_changelog.xml diff --git a/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java b/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java index 68631fc59d83..f45702fbcd71 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java @@ -24,6 +24,7 @@ public class LearningPath extends DomainObject { @ManyToOne + @JoinColumn(name = "user_id") @JsonIgnore private User user; diff --git a/src/main/resources/config/liquibase/changelog/20230622000000_changelog.xml b/src/main/resources/config/liquibase/changelog/20230622000000_changelog.xml new file mode 100644 index 000000000000..6c12f389d3d6 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20230622000000_changelog.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index 31950a38c92e..7ff180295682 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -38,6 +38,7 @@ + From 4a1d22d99bdff841d8f7f2477f77a9c4d896e23e Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 22 Jun 2023 22:19:52 +0200 Subject: [PATCH 010/215] Remove redundant equals and hashCode methods from LearningPath --- .../domain/competency/LearningPath.java | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java b/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java index f45702fbcd71..9ae5ab45ce44 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java @@ -1,7 +1,6 @@ package de.tum.in.www1.artemis.domain.competency; import java.util.HashSet; -import java.util.Objects; import java.util.Set; import javax.persistence.*; @@ -63,23 +62,6 @@ public void setCompetencies(Set competencies) { this.competencies = competencies; } - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - LearningPath that = (LearningPath) obj; - return getId().equals(that.getId()); - } - - @Override - public int hashCode() { - return Objects.hashCode(getId()); - } - @Override public String toString() { return "LearningPath{" + "id=" + getId() + ", user=" + user + ", course=" + course + ", competencies=" + competencies + '}'; From 484b05cb2349674da3a8694de5df6522cc06a340 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 22 Jun 2023 22:21:00 +0200 Subject: [PATCH 011/215] Fix access modifier of LearningPathIntegrationTest --- .../in/www1/artemis/lecture/LearningPathIntegrationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java index c9ad61881bc2..b952660d9eee 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java @@ -22,7 +22,7 @@ import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.util.PageableSearchUtilService; -public class LearningPathIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class LearningPathIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { private static final String TEST_PREFIX = "learningpathintegration"; From ae984fbb4ba9b0c26426f795a6544c95a3c8803b Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Fri, 23 Jun 2023 12:14:00 +0200 Subject: [PATCH 012/215] Update LearningPathResource.java --- .../de/tum/in/www1/artemis/web/rest/LearningPathResource.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java index b17e5273e5d9..2873be5fb06c 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java @@ -72,12 +72,12 @@ public ResponseEntity enableLearningPathsForCourse(@PathVariable Long co } /** - * GET /course/:courseId/learning-paths : gets all the learning paths of a course + * GET /courses/:courseId/learning-paths : gets all the learning paths of a course * * @param courseId the id of the course for which the learning paths should be fetched * @return the ResponseEntity with status 200 (OK) and with body the desired page, sorted and matching the given query */ - @GetMapping("/course/{courseId}/learning-paths") + @GetMapping("/courses/{courseId}/learning-paths") @EnforceAtLeastInstructor public ResponseEntity> getLearningPathsOnPage(@PathVariable Long courseId, PageableSearchDTO search) { log.debug("REST request to get learning paths for course with id: {}", courseId); From f2dc4a3232023b4a392145a0210c90298e8f1220 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Fri, 23 Jun 2023 12:14:06 +0200 Subject: [PATCH 013/215] Update LearningPathIntegrationTest.java --- .../lecture/LearningPathIntegrationTest.java | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java index b952660d9eee..d10df2d088de 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java @@ -94,9 +94,9 @@ private ZonedDateTime future(long days) { } private void testAllPreAuthorize() throws Exception { - request.putWithResponseBody("/courses/" + course.getId() + "/learning-paths/enable", null, Course.class, HttpStatus.FORBIDDEN); + request.putWithResponseBody("/api/courses/" + course.getId() + "/learning-paths/enable", null, Course.class, HttpStatus.FORBIDDEN); final var search = pageableSearchUtilService.configureSearch(""); - request.getSearchResult("/courses/" + course.getId() + "/learning-paths", HttpStatus.FORBIDDEN, LearningPath.class, pageableSearchUtilService.searchMapping(search)); + request.getSearchResult("/api/courses/" + course.getId() + "/learning-paths", HttpStatus.FORBIDDEN, LearningPath.class, pageableSearchUtilService.searchMapping(search)); } @Test @@ -120,7 +120,7 @@ void testAll_asEditor() throws Exception { @Test @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") void testEnableLearningPaths() throws Exception { - request.putWithResponseBody("/courses/" + course.getId() + "/learning-paths/enable", course, Course.class, HttpStatus.OK); + request.putWithResponseBody("/api/courses/" + course.getId() + "/learning-paths/enable", course, Course.class, HttpStatus.OK); final var updatedCourse = courseRepository.findByIdElseThrow(course.getId()); assertThat(updatedCourse.getLearningPathsEnabled()).isTrue().as("should enable LearningPaths"); assertThat(updatedCourse.getLearningPaths()).isNotNull(); @@ -132,12 +132,17 @@ void testEnableLearningPaths() throws Exception { void testEnableLearningPathsAlreadyEnabled() throws Exception { course.setLeanringPathsEnabled(true); courseRepository.save(course); - request.putWithResponseBody("/courses/" + course.getId() + "/learning-paths/enable", course, Course.class, HttpStatus.BAD_REQUEST); + request.putWithResponseBody("/api/courses/" + course.getId() + "/learning-paths/enable", course, Course.class, HttpStatus.BAD_REQUEST); } @Test @WithMockUser(username = TEST_PREFIX + "student1337", roles = "USER") void testGenerateLearningPathOnEnrollment() throws Exception { + course.setEnrollmentEnabled(true); + course.setEnrollmentStartDate(past(1)); + course.setEnrollmentEndDate(future(1)); + course = courseRepository.save(course); + enableLearningPathsForTestingCourse(); final var updatedStudent = request.postWithResponseBody("/api/courses/" + course.getId() + "/enroll", null, User.class, HttpStatus.OK); assertThat(updatedStudent.getLearningPaths()).isNotNull(); assertThat(updatedStudent.getLearningPaths().size()).isEqualTo(1).as("should create LearningPath for student"); @@ -147,7 +152,7 @@ void testGenerateLearningPathOnEnrollment() throws Exception { @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") void testGetLearningPathsOnPageForCourseLearningPathsDisabled() throws Exception { final var search = pageableSearchUtilService.configureSearch(""); - request.getSearchResult("/courses/" + course.getId() + "/learning-paths", HttpStatus.BAD_REQUEST, LearningPath.class, pageableSearchUtilService.searchMapping(search)); + request.getSearchResult("/api/courses/" + course.getId() + "/learning-paths", HttpStatus.BAD_REQUEST, LearningPath.class, pageableSearchUtilService.searchMapping(search)); } @Test @@ -155,7 +160,7 @@ void testGetLearningPathsOnPageForCourseLearningPathsDisabled() throws Exception void testGetLearningPathsOnPageForCourseEmpty() throws Exception { enableLearningPathsForTestingCourse(); final var search = pageableSearchUtilService.configureSearch(STUDENT_OF_COURSE + "SuffixThatAllowsTheResultToBeEmpty"); - final var result = request.getSearchResult("/courses/" + course.getId() + "/learning-paths", HttpStatus.OK, LearningPath.class, + final var result = request.getSearchResult("/api/courses/" + course.getId() + "/learning-paths", HttpStatus.OK, LearningPath.class, pageableSearchUtilService.searchMapping(search)); assertThat(result.getResultsOnPage()).isNullOrEmpty(); } @@ -165,7 +170,7 @@ void testGetLearningPathsOnPageForCourseEmpty() throws Exception { void testGetLearningPathsOnPageForCourseExactlyStudent() throws Exception { enableLearningPathsForTestingCourse(); final var search = pageableSearchUtilService.configureSearch(STUDENT_OF_COURSE); - final var result = request.getSearchResult("/courses/" + course.getId() + "/learning-paths", HttpStatus.OK, LearningPath.class, + final var result = request.getSearchResult("/api/courses/" + course.getId() + "/learning-paths", HttpStatus.OK, LearningPath.class, pageableSearchUtilService.searchMapping(search)); assertThat(result.getResultsOnPage()).hasSize(1); } From 0fa0e33290f0482725b24b22f1a2f43d4cc2a286 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Sat, 24 Jun 2023 13:14:17 +0200 Subject: [PATCH 014/215] Update src/main/resources/config/liquibase/changelog/20230622000000_changelog.xml --- .../config/liquibase/changelog/20230622000000_changelog.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/config/liquibase/changelog/20230622000000_changelog.xml b/src/main/resources/config/liquibase/changelog/20230622000000_changelog.xml index 6c12f389d3d6..b349a86d4771 100644 --- a/src/main/resources/config/liquibase/changelog/20230622000000_changelog.xml +++ b/src/main/resources/config/liquibase/changelog/20230622000000_changelog.xml @@ -17,7 +17,7 @@ - + From 46a30728038dbb3ad318a173f5a2dc39477b70ad Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Sat, 24 Jun 2023 20:09:12 +0200 Subject: [PATCH 015/215] Add unique cosntraint to changelog --- .../config/liquibase/changelog/20230622000000_changelog.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/config/liquibase/changelog/20230622000000_changelog.xml b/src/main/resources/config/liquibase/changelog/20230622000000_changelog.xml index b349a86d4771..6c3ddebd0c97 100644 --- a/src/main/resources/config/liquibase/changelog/20230622000000_changelog.xml +++ b/src/main/resources/config/liquibase/changelog/20230622000000_changelog.xml @@ -15,6 +15,7 @@ + From f17de46d248852f1becc71a5c69ac29f49913294 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Sat, 24 Jun 2023 20:29:33 +0200 Subject: [PATCH 016/215] Remove parallel stream --- .../de/tum/in/www1/artemis/service/LearningPathService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index 8d304b26197a..289284f0df35 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -37,7 +37,7 @@ public LearningPathService(UserRepository userRepository, LearningPathRepository */ public void generateLearningPaths(@NotNull Course course) { var students = userRepository.getStudents(course); - students.stream().parallel().forEach((student) -> { + students.forEach((student) -> { generateLearningPathForUser(course, student); }); log.debug("Successfully created learning paths for all {} students in course (id={})", students.size(), course.getId()); From ed72a0462b0aa2c181387cde5eaac37f3f5bac94 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Sat, 24 Jun 2023 20:30:55 +0200 Subject: [PATCH 017/215] Remove unnecessary json view annotation --- .../de/tum/in/www1/artemis/domain/competency/LearningPath.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java b/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java index 9ae5ab45ce44..6f13e9c85e54 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java @@ -10,12 +10,10 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonView; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.DomainObject; import de.tum.in.www1.artemis.domain.User; -import de.tum.in.www1.artemis.domain.view.QuizView; @Entity @Table(name = "learning_path") @@ -35,7 +33,6 @@ public class LearningPath extends DomainObject { @ManyToMany @JoinTable(name = "competency_learning_path", joinColumns = @JoinColumn(name = "learning_path_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "competency_id", referencedColumnName = "id")) @JsonIgnoreProperties({ "exercises", "course", "learningPaths" }) - @JsonView(QuizView.Before.class) private Set competencies = new HashSet<>(); public User getUser() { From 0c922126a5923a5309a08f090dfd011a152b762f Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Mon, 26 Jun 2023 15:15:19 +0200 Subject: [PATCH 018/215] Update LearningPathService.java --- .../de/tum/in/www1/artemis/service/LearningPathService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index 289284f0df35..d3e0e069aac8 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -55,7 +55,7 @@ public void generateLearningPathForUser(@NotNull Course course, @NotNull User us LearningPath lpToCreate = new LearningPath(); lpToCreate.setUser(user); lpToCreate.setCourse(course); - lpToCreate.setCompetencies(course.getCompetencies()); + lpToCreate.getCompetencies().addAll(course.getCompetencies()); var persistedLearningPath = learningPathRepository.save(lpToCreate); log.debug("Created LearningPath (id={}) for user (id={}) in course (id={})", persistedLearningPath.getId(), user.getId(), course.getId()); } From c2f958585fe4ca04d917f951ddf4ffb4113b97dc Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Mon, 26 Jun 2023 15:49:28 +0200 Subject: [PATCH 019/215] Fix loading courses with eager learning paths --- .../tum/in/www1/artemis/repository/CourseRepository.java | 8 ++++++++ .../in/www1/artemis/web/rest/LearningPathResource.java | 2 +- .../www1/artemis/lecture/LearningPathIntegrationTest.java | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java index 95941880042e..e8d12facc532 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java @@ -154,6 +154,9 @@ SELECT CASE WHEN (count(c) > 0) THEN true ELSE false END @EntityGraph(type = LOAD, attributePaths = { "competencies", "prerequisites" }) Optional findWithEagerCompetenciesById(long courseId); + @EntityGraph(type = LOAD, attributePaths = { "learningPaths" }) + Optional findWithEagerLearningPathsById(long courseId); + @EntityGraph(type = LOAD, attributePaths = { "lectures" }) Optional findWithEagerLecturesById(long courseId); @@ -416,6 +419,11 @@ default Course findWithEagerCompetenciesByIdElseThrow(long courseId) { return findWithEagerCompetenciesById(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId)); } + @NotNull + default Course findWithEagerLearningPathsByIdElseThrow(long courseId) { + return findWithEagerLearningPathsById(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId)); + } + /** * Checks if the messaging feature is enabled for a course. * diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java index 2873be5fb06c..ed8181472f3d 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java @@ -56,7 +56,7 @@ public LearningPathResource(CourseRepository courseRepository, AuthorizationChec @EnforceAtLeastInstructor public ResponseEntity enableLearningPathsForCourse(@PathVariable Long courseId) { log.debug("REST request to enable learning paths for course with id: {}", courseId); - Course course = courseRepository.findByIdElseThrow(courseId); + Course course = courseRepository.findWithEagerCompetenciesByIdElseThrow(courseId); User user = userRepository.getUserWithGroupsAndAuthorities(); authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, user); if (course.getLearningPathsEnabled()) { diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java index d10df2d088de..835c561e1b1e 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java @@ -121,7 +121,7 @@ void testAll_asEditor() throws Exception { @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") void testEnableLearningPaths() throws Exception { request.putWithResponseBody("/api/courses/" + course.getId() + "/learning-paths/enable", course, Course.class, HttpStatus.OK); - final var updatedCourse = courseRepository.findByIdElseThrow(course.getId()); + final var updatedCourse = courseRepository.findWithEagerLearningPathsByIdElseThrow(course.getId()); assertThat(updatedCourse.getLearningPathsEnabled()).isTrue().as("should enable LearningPaths"); assertThat(updatedCourse.getLearningPaths()).isNotNull(); assertThat(updatedCourse.getLearningPaths().size()).isEqualTo(NUMBER_OF_STUDENTS).as("should create LearningPath for each student"); From 08e5ca8312a0224ca8bd7893c8f10dfedecf5145 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Mon, 26 Jun 2023 16:46:00 +0200 Subject: [PATCH 020/215] Fix learning path generation on enrollment --- .../artemis/repository/CourseRepository.java | 15 ++++++++++ .../artemis/repository/UserRepository.java | 14 +++++++++ .../www1/artemis/web/rest/CourseResource.java | 2 +- .../lecture/LearningPathIntegrationTest.java | 30 ++++++++++++++++--- 4 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java index e8d12facc532..01cf2521784a 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java @@ -189,6 +189,16 @@ SELECT CASE WHEN (count(c) > 0) THEN true ELSE false END """) Optional findWithEagerOrganizations(@Param("courseId") long courseId); + @Query(""" + SELECT course + FROM Course course + LEFT JOIN FETCH course.organizations + LEFT JOIN FETCH course.competencies + LEFT JOIN FETCH course.learningPaths + WHERE course.id = :courseId + """) + Optional findWithEagerOrganizationsAndCompetenciesAndLearningPaths(@Param("courseId") long courseId); + @EntityGraph(type = LOAD, attributePaths = { "onlineCourseConfiguration", "tutorialGroupsConfiguration" }) Course findWithEagerOnlineCourseConfigurationAndTutorialGroupConfigurationById(long courseId); @@ -329,6 +339,11 @@ default Course findWithEagerOrganizationsElseThrow(long courseId) throws EntityN return findWithEagerOrganizations(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId)); } + @NotNull + default Course findWithEagerOrganizationsAndCompetenciesAndLearningPathsElseThrow(long courseId) throws EntityNotFoundException { + return findWithEagerOrganizationsAndCompetenciesAndLearningPaths(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId)); + } + /** * filters the passed exercises for the relevant ones that need to be manually assessed. This excludes quizzes and automatic programming exercises * diff --git a/src/main/java/de/tum/in/www1/artemis/repository/UserRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/UserRepository.java index bae8840e8755..a13282526263 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/UserRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/UserRepository.java @@ -97,6 +97,9 @@ public interface UserRepository extends JpaRepository, JpaSpecificat @EntityGraph(type = LOAD, attributePaths = { "groups", "authorities", "guidedTourSettings" }) Optional findOneWithGroupsAuthoritiesAndGuidedTourSettingsByLogin(String login); + @EntityGraph(type = LOAD, attributePaths = { "learningPaths" }) + Optional findWithLearningPathsById(long id); + @Query("SELECT count(*) FROM User user WHERE user.isDeleted = false AND :#{#groupName} MEMBER OF user.groups") Long countByGroupsIsContaining(@Param("groupName") String groupName); @@ -534,6 +537,17 @@ default User findByIdWithGroupsAndAuthoritiesAndOrganizationsElseThrow(long user return findOneWithGroupsAndAuthoritiesAndOrganizationsById(userId).orElseThrow(() -> new EntityNotFoundException("User", userId)); } + /** + * Find user with eagerly loaded learning paths by its id + * + * @param userId the id of the user to find + * @return the user with learning paths if it exists, else throw exception + */ + @NotNull + default User findWithLearningPathsByIdElseThrow(long userId) { + return findWithLearningPathsById(userId).orElseThrow(() -> new EntityNotFoundException("User", userId)); + } + /** * Get students by given course * diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java index d5b41ddb6922..6a87454ca3b7 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java @@ -301,7 +301,7 @@ public ResponseEntity updateOnlineCourseConfiguration @PostMapping("courses/{courseId}/enroll") @EnforceAtLeastStudent public ResponseEntity enrollInCourse(@PathVariable Long courseId) { - Course course = courseRepository.findWithEagerOrganizationsElseThrow(courseId); + Course course = courseRepository.findWithEagerOrganizationsAndCompetenciesAndLearningPathsElseThrow(courseId); User user = userRepository.getUserWithGroupsAndAuthoritiesAndOrganizations(); log.debug("REST request to enroll {} in Course {}", user.getName(), course.getTitle()); courseService.enrollUserForCourseOrThrow(user, course); diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java index 835c561e1b1e..06045dcf5303 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java @@ -2,7 +2,9 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.net.URISyntaxException; import java.time.ZonedDateTime; +import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -10,6 +12,8 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; +import com.fasterxml.jackson.core.JsonProcessingException; + import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; import de.tum.in.www1.artemis.competency.CompetencyUtilService; import de.tum.in.www1.artemis.course.CourseUtilService; @@ -18,6 +22,7 @@ import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.competency.LearningPath; import de.tum.in.www1.artemis.repository.CourseRepository; +import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.service.LearningPathService; import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.util.PageableSearchUtilService; @@ -29,6 +34,9 @@ class LearningPathIntegrationTest extends AbstractSpringIntegrationBambooBitbuck @Autowired private CourseRepository courseRepository; + @Autowired + private UserRepository userRepository; + @Autowired private LearningPathService learningPathService; @@ -58,6 +66,8 @@ class LearningPathIntegrationTest extends AbstractSpringIntegrationBambooBitbuck private final String INSTRUCTOR_OF_COURSE = TEST_PREFIX + "instructor1"; + private User studentNotInCourse; + @BeforeEach void setupTestScenario() { participantScoreScheduleService.activate(); @@ -65,7 +75,7 @@ void setupTestScenario() { userUtilService.addUsers(TEST_PREFIX, NUMBER_OF_STUDENTS, 1, 1, 1); // Add users that are not in the course - userUtilService.createAndSaveUser(TEST_PREFIX + "student1337"); + studentNotInCourse = userUtilService.createAndSaveUser(TEST_PREFIX + "student1337"); userUtilService.createAndSaveUser(TEST_PREFIX + "instructor1337"); // creating course @@ -143,9 +153,21 @@ void testGenerateLearningPathOnEnrollment() throws Exception { course.setEnrollmentEndDate(future(1)); course = courseRepository.save(course); enableLearningPathsForTestingCourse(); - final var updatedStudent = request.postWithResponseBody("/api/courses/" + course.getId() + "/enroll", null, User.class, HttpStatus.OK); - assertThat(updatedStudent.getLearningPaths()).isNotNull(); - assertThat(updatedStudent.getLearningPaths().size()).isEqualTo(1).as("should create LearningPath for student"); + + this.setupEnrollmentRequestMocks(); + + final var updatedUser = request.postWithResponseBody("/api/courses/" + course.getId() + "/enroll", null, User.class, HttpStatus.OK); + final var updatedUserWithLearningPaths = userRepository.findWithLearningPathsByIdElseThrow(updatedUser.getId()); + assertThat(updatedUserWithLearningPaths.getLearningPaths()).isNotNull(); + assertThat(updatedUserWithLearningPaths.getLearningPaths().size()).isEqualTo(1).as("should create LearningPath for student"); + } + + private void setupEnrollmentRequestMocks() throws JsonProcessingException, URISyntaxException { + jiraRequestMockProvider.enableMockingOfRequests(); + jiraRequestMockProvider.mockAddUserToGroupForMultipleGroups(Set.of(course.getStudentGroupName())); + bitbucketRequestMockProvider.enableMockingOfRequests(); + bitbucketRequestMockProvider.mockUpdateUserDetails(studentNotInCourse.getLogin(), studentNotInCourse.getEmail(), studentNotInCourse.getName()); + bitbucketRequestMockProvider.mockAddUserToGroups(); } @Test From f533542f4c50d5daf90d36e67830f8d868bd7b61 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Mon, 26 Jun 2023 17:20:43 +0200 Subject: [PATCH 021/215] Optimise stream for CompetencyProgress calculation --- .../tum/in/www1/artemis/service/CompetencyProgressService.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/CompetencyProgressService.java b/src/main/java/de/tum/in/www1/artemis/service/CompetencyProgressService.java index 991ea8495798..3892c3fb4398 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/CompetencyProgressService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/CompetencyProgressService.java @@ -229,8 +229,7 @@ public CompetencyProgress updateCompetencyProgress(Long competencyId, User user) * @return The percentage of completed learning objects by the user */ private double calculateProgress(@NotNull List learningObjects, @NotNull User user) { - var completions = learningObjects.stream().map(learningObject -> hasUserCompleted(user, learningObject)).toList(); - return completions.stream().mapToInt(completed -> completed ? 100 : 0).summaryStatistics().getAverage(); + return learningObjects.stream().map(learningObject -> hasUserCompleted(user, learningObject)).mapToInt(completed -> completed ? 100 : 0).summaryStatistics().getAverage(); } /** From 01f1af29dac3b11b77e372d5fe7eaefb713cec72 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Tue, 27 Jun 2023 16:23:35 +0200 Subject: [PATCH 022/215] Add mechanism to update learning paths --- .../domain/competency/LearningPath.java | 19 +++++ .../CompetencyProgressRepository.java | 9 +++ .../repository/LearningPathRepository.java | 22 ++++++ .../service/CompetencyProgressService.java | 19 ++++- .../artemis/service/LearningPathService.java | 69 ++++++++++++++++++- .../artemis/web/rest/CompetencyResource.java | 21 ++++-- .../web/rest/LearningPathResource.java | 12 +--- .../changelog/20230622000000_changelog.xml | 1 + 8 files changed, 155 insertions(+), 17 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java b/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java index 6f13e9c85e54..18de919a47f0 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java @@ -20,6 +20,9 @@ @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) public class LearningPath extends DomainObject { + @Column(name = "mastered_competencies") + private int masteredCompetencies; + @ManyToOne @JoinColumn(name = "user_id") @JsonIgnore @@ -35,6 +38,14 @@ public class LearningPath extends DomainObject { @JsonIgnoreProperties({ "exercises", "course", "learningPaths" }) private Set competencies = new HashSet<>(); + public int getMasteredCompetencies() { + return masteredCompetencies; + } + + public void setMasteredCompetencies(int masteredCompetencies) { + this.masteredCompetencies = masteredCompetencies; + } + public User getUser() { return user; } @@ -59,6 +70,14 @@ public void setCompetencies(Set competencies) { this.competencies = competencies; } + public void addCompetency(Competency competency) { + this.competencies.add(competency); + } + + public void removeCompetency(Competency competency) { + this.competencies.remove(competency); + } + @Override public String toString() { return "LearningPath{" + "id=" + getId() + ", user=" + user + ", course=" + course + ", competencies=" + competencies + '}'; diff --git a/src/main/java/de/tum/in/www1/artemis/repository/CompetencyProgressRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/CompetencyProgressRepository.java index 3ab0a204049f..17227016cdb4 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/CompetencyProgressRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/CompetencyProgressRepository.java @@ -2,6 +2,7 @@ import java.util.List; import java.util.Optional; +import java.util.Set; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; @@ -49,6 +50,14 @@ public interface CompetencyProgressRepository extends JpaRepository findEagerByCompetencyIdAndUserId(@Param("competencyId") Long competencyId, @Param("userId") Long userId); + @Query(""" + SELECT cp + FROM CompetencyProgress cp + WHERE cp.learningGoal.id IN :competencyIds + AND cp.user.id = :userId + """) + List findAllByCompetencyIdsAndUserId(@Param("competencyId") Set competencyIds, @Param("userId") Long userId); + @Query(""" SELECT AVG(cp.confidence) FROM CompetencyProgress cp diff --git a/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java index 95c110d2e821..270b84e66515 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java @@ -1,22 +1,36 @@ package de.tum.in.www1.artemis.repository; +import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.LOAD; + import java.util.List; +import java.util.Optional; import java.util.Set; +import javax.validation.constraints.NotNull; + import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; 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.competency.LearningPath; +import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; @Repository public interface LearningPathRepository extends JpaRepository { List findByCourseIdAndUserId(long courseId, long userId); + @EntityGraph(type = LOAD, attributePaths = { "competencies" }) + Optional findWithEagerCompetenciesByCourseIdAndUserId(long courseId, long userId); + + default LearningPath findWithEagerCompetenciesByCourseIdAndUserIdElseThrow(long courseId, long userId) { + return findWithEagerCompetenciesByCourseIdAndUserId(courseId, userId).orElseThrow(() -> new EntityNotFoundException("LearningPath")); + } + @Query(""" SELECT lp FROM LearningPath lp @@ -30,4 +44,12 @@ public interface LearningPathRepository extends JpaRepository findByLoginInCourse(@Param("partialLogin") String partialLogin, @Param("courseId") long courseId, Pageable pageable); + + @EntityGraph(type = LOAD, attributePaths = { "competencies" }) + Optional findWithEagerCompetenciesById(long learningPathId); + + @NotNull + default LearningPath findWithEagerCompetenciesByIdElseThrow(long learningPathId) { + return findWithEagerCompetenciesById(learningPathId).orElseThrow(() -> new EntityNotFoundException("LearningPath", learningPathId)); + } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/CompetencyProgressService.java b/src/main/java/de/tum/in/www1/artemis/service/CompetencyProgressService.java index 3892c3fb4398..7277448eb189 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/CompetencyProgressService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/CompetencyProgressService.java @@ -46,9 +46,11 @@ public class CompetencyProgressService { private final UserRepository userRepository; + private final LearningPathService learningPathService; + public CompetencyProgressService(CompetencyRepository competencyRepository, CompetencyProgressRepository competencyProgressRepository, StudentScoreRepository studentScoreRepository, TeamScoreRepository teamScoreRepository, ExerciseRepository exerciseRepository, - LectureUnitRepository lectureUnitRepository, UserRepository userRepository) { + LectureUnitRepository lectureUnitRepository, UserRepository userRepository, LearningPathService learningPathService) { this.competencyRepository = competencyRepository; this.competencyProgressRepository = competencyProgressRepository; this.studentScoreRepository = studentScoreRepository; @@ -56,6 +58,7 @@ public CompetencyProgressService(CompetencyRepository competencyRepository, Comp this.exerciseRepository = exerciseRepository; this.lectureUnitRepository = lectureUnitRepository; this.userRepository = userRepository; + this.learningPathService = learningPathService; } /** @@ -218,6 +221,8 @@ public CompetencyProgress updateCompetencyProgress(Long competencyId, User user) logger.debug("Updated progress for user {} in competency {} to {} / {}.", user.getLogin(), competency.getId(), studentProgress.getProgress(), studentProgress.getConfidence()); + + learningPathService.updateLearningPathProgress(competency.getCourse().getId(), user.getId()); return studentProgress; } @@ -264,4 +269,16 @@ else if (learningObject instanceof Exercise exercise) { throw new IllegalArgumentException("Learning object must be either LectureUnit or Exercise"); } + /** + * Checks if the user associated to this {@code CompetencyProgress} has mastered the associated {@code Competency}. + * + * @param competencyProgress The user's progress + * @return True if the user mastered the competency, false otherwise + */ + public static boolean isMastered(@NotNull CompetencyProgress competencyProgress) { + final double weight = 2.0 / 3.0; + final double mastery = (1 - weight) * competencyProgress.getProgress() + weight * competencyProgress.getConfidence(); + return mastery >= competencyProgress.getCompetency().getMasteryThreshold(); + } + } diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index d3e0e069aac8..7ffa552a90e2 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -1,5 +1,7 @@ package de.tum.in.www1.artemis.service; +import java.util.stream.Collectors; + import javax.validation.constraints.NotNull; import org.slf4j.Logger; @@ -9,9 +11,9 @@ import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.User; +import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.competency.LearningPath; -import de.tum.in.www1.artemis.repository.LearningPathRepository; -import de.tum.in.www1.artemis.repository.UserRepository; +import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.web.rest.dto.PageableSearchDTO; import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; import de.tum.in.www1.artemis.web.rest.util.PageUtil; @@ -25,9 +27,16 @@ public class LearningPathService { private final LearningPathRepository learningPathRepository; - public LearningPathService(UserRepository userRepository, LearningPathRepository learningPathRepository) { + private final CompetencyProgressRepository competencyProgressRepository; + + private final CourseRepository courseRepository; + + public LearningPathService(UserRepository userRepository, LearningPathRepository learningPathRepository, CompetencyProgressRepository competencyProgressRepository, + CourseRepository courseRepository) { this.userRepository = userRepository; this.learningPathRepository = learningPathRepository; + this.competencyProgressRepository = competencyProgressRepository; + this.courseRepository = courseRepository; } /** @@ -73,4 +82,58 @@ public SearchResultPageDTO getAllOfCourseOnPageWithSize(final Page final Page learningPathPage = learningPathRepository.findByLoginInCourse(searchTerm, course.getId(), pageable); return new SearchResultPageDTO<>(learningPathPage.getContent(), learningPathPage.getTotalPages()); } + + /** + * Links given competency to all learning paths of the course. + * + * @param competency Competency that should be added to each learning path + * @param courseId course id that the learning paths belong to + */ + public void linkCompetencyToLearningPathsOfCourse(Competency competency, long courseId) { + var course = courseRepository.findWithEagerLearningPathsByIdElseThrow(courseId); + var learningPaths = course.getLearningPaths(); + learningPaths.forEach(learningPath -> { + learningPath.addCompetency(competency); + }); + learningPathRepository.saveAll(learningPaths); + log.debug("Linked competency (id={}) to learning paths", competency.getId()); + } + + /** + * Remove linked competency from all learning paths of the course. + * + * @param competency Competency that should be removed from each learning path + * @param courseId course id that the learning paths belong to + */ + public void removeLinkedCompetencyFromLearningPathsOfCourse(Competency competency, long courseId) { + var course = courseRepository.findWithEagerLearningPathsByIdElseThrow(courseId); + var learningPaths = course.getLearningPaths(); + learningPaths.forEach(learningPath -> { + learningPath.removeCompetency(competency); + }); + learningPathRepository.saveAll(learningPaths); + log.debug("Removed linked competency (id={}) from learning paths", competency.getId()); + } + + public void updateLearningPathProgress(final long learningPathId) { + final var learningPath = learningPathRepository.findWithEagerCompetenciesByIdElseThrow(learningPathId); + this.updateLearningPathProgress(learningPath); + } + + public void updateLearningPathProgress(final long courseId, final long userId) { + final var learningPath = learningPathRepository.findWithEagerCompetenciesByCourseIdAndUserId(courseId, userId); + learningPath.ifPresent(this::updateLearningPathProgress); + } + + private void updateLearningPathProgress(final LearningPath learningPath) { + final var userId = learningPath.getUser().getId(); + final var competencyIds = learningPath.getCompetencies().stream().map(Competency::getId).collect(Collectors.toSet()); + final var progress = competencyProgressRepository.findAllByCompetencyIdsAndUserId(competencyIds, userId); + + // TODO: consider optional competencies + final var mastered = (int) progress.stream().filter(CompetencyProgressService::isMastered).count(); + learningPath.setMasteredCompetencies(mastered); + learningPathRepository.save(learningPath); + log.debug("Updated LearningPath (id={}) for user (id={})", learningPath.getId(), userId); + } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java index 1400be390aa7..7c7a05c4f4e6 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java @@ -25,9 +25,7 @@ import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastEditor; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent; -import de.tum.in.www1.artemis.service.AuthorizationCheckService; -import de.tum.in.www1.artemis.service.CompetencyProgressService; -import de.tum.in.www1.artemis.service.CompetencyService; +import de.tum.in.www1.artemis.service.*; import de.tum.in.www1.artemis.service.util.RoundingUtil; import de.tum.in.www1.artemis.web.rest.dto.CourseCompetencyProgressDTO; import de.tum.in.www1.artemis.web.rest.dto.PageableSearchDTO; @@ -66,10 +64,12 @@ public class CompetencyResource { private final CompetencyProgressService competencyProgressService; + private final LearningPathService learningPathService; + public CompetencyResource(CourseRepository courseRepository, AuthorizationCheckService authorizationCheckService, UserRepository userRepository, CompetencyRepository competencyRepository, CompetencyRelationRepository competencyRelationRepository, LectureUnitRepository lectureUnitRepository, CompetencyService competencyService, CompetencyProgressRepository competencyProgressRepository, ExerciseRepository exerciseRepository, - CompetencyProgressService competencyProgressService) { + CompetencyProgressService competencyProgressService, LearningPathService learningPathService) { this.courseRepository = courseRepository; this.competencyRelationRepository = competencyRelationRepository; this.lectureUnitRepository = lectureUnitRepository; @@ -80,6 +80,7 @@ public CompetencyResource(CourseRepository courseRepository, AuthorizationCheckS this.competencyProgressRepository = competencyProgressRepository; this.exerciseRepository = exerciseRepository; this.competencyProgressService = competencyProgressService; + this.learningPathService = learningPathService; } /** @@ -217,6 +218,10 @@ public ResponseEntity createCompetency(@PathVariable Long courseId, linkLectureUnitsToCompetency(persistedCompetency, competency.getLectureUnits(), Set.of()); + if (course.getLearningPathsEnabled()) { + learningPathService.linkCompetencyToLearningPathsOfCourse(persistedCompetency, courseId); + } + return ResponseEntity.created(new URI("/api/courses/" + courseId + "/competencies/" + persistedCompetency.getId())).body(persistedCompetency); } @@ -245,6 +250,10 @@ public ResponseEntity importCompetency(@PathVariable long courseId, competencyToImport.setId(null); competencyToImport = competencyRepository.save(competencyToImport); + if (course.getLearningPathsEnabled()) { + learningPathService.linkCompetencyToLearningPathsOfCourse(competencyToImport, courseId); + } + return ResponseEntity.created(new URI("/api/courses/" + courseId + "/competencies/" + competencyToImport.getId())).body(competencyToImport); } @@ -281,6 +290,10 @@ public ResponseEntity deleteCompetency(@PathVariable Long competencyId, @P lectureUnitRepository.save(lectureUnit); }); + if (course.getComplaintsEnabled()) { + learningPathService.removeLinkedCompetencyFromLearningPathsOfCourse(competency, courseId); + } + competencyRepository.deleteById(competency.getId()); return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, competency.getTitle())).build(); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java index ed8181472f3d..f37c31f7b7e1 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java @@ -30,24 +30,18 @@ public class LearningPathResource { private final UserRepository userRepository; - private final CompetencyRepository competencyRepository; - - private final LearningPathRepository learningPathRepository; - private final LearningPathService learningPathService; public LearningPathResource(CourseRepository courseRepository, AuthorizationCheckService authorizationCheckService, UserRepository userRepository, - CompetencyRepository competencyRepository, LearningPathService learningPathService, LearningPathRepository learningPathRepository) { + LearningPathService learningPathService) { this.courseRepository = courseRepository; this.authorizationCheckService = authorizationCheckService; this.userRepository = userRepository; - this.competencyRepository = competencyRepository; this.learningPathService = learningPathService; - this.learningPathRepository = learningPathRepository; } /** - * PUT /courses/:courseId/learning-paths/enable : enables and generates learning paths for the course + * PUT /courses/:courseId/learning-paths/enable : Enables and generates learning paths for the course * * @param courseId the id of the course for which the learning paths should be enabled * @return the ResponseEntity with status 200 (OK) and with body the updated course @@ -72,7 +66,7 @@ public ResponseEntity enableLearningPathsForCourse(@PathVariable Long co } /** - * GET /courses/:courseId/learning-paths : gets all the learning paths of a course + * GET /courses/:courseId/learning-paths : Gets all the learning paths of a course. The result is pageable. * * @param courseId the id of the course for which the learning paths should be fetched * @return the ResponseEntity with status 200 (OK) and with body the desired page, sorted and matching the given query diff --git a/src/main/resources/config/liquibase/changelog/20230622000000_changelog.xml b/src/main/resources/config/liquibase/changelog/20230622000000_changelog.xml index 6c3ddebd0c97..794742dae9c4 100644 --- a/src/main/resources/config/liquibase/changelog/20230622000000_changelog.xml +++ b/src/main/resources/config/liquibase/changelog/20230622000000_changelog.xml @@ -14,6 +14,7 @@ + From 3606e673211ce6a4e4a304490470d764e67173a4 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Wed, 28 Jun 2023 12:37:50 +0200 Subject: [PATCH 023/215] improve code quality --- .../artemis/repository/CourseRepository.java | 8 ++++---- .../repository/LearningPathRepository.java | 3 +-- .../artemis/service/LearningPathService.java | 20 +++++++------------ 3 files changed, 12 insertions(+), 19 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java index 01cf2521784a..3e3fe087d540 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java @@ -154,8 +154,8 @@ SELECT CASE WHEN (count(c) > 0) THEN true ELSE false END @EntityGraph(type = LOAD, attributePaths = { "competencies", "prerequisites" }) Optional findWithEagerCompetenciesById(long courseId); - @EntityGraph(type = LOAD, attributePaths = { "learningPaths" }) - Optional findWithEagerLearningPathsById(long courseId); + @EntityGraph(type = LOAD, attributePaths = { "competencies", "learningPaths", "learningPaths.competencies" }) + Optional findWithEagerLearningPathsAndCompetenciesById(long courseId); @EntityGraph(type = LOAD, attributePaths = { "lectures" }) Optional findWithEagerLecturesById(long courseId); @@ -435,8 +435,8 @@ default Course findWithEagerCompetenciesByIdElseThrow(long courseId) { } @NotNull - default Course findWithEagerLearningPathsByIdElseThrow(long courseId) { - return findWithEagerLearningPathsById(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId)); + default Course findWithEagerLearningPathsAndCompetenciesByIdElseThrow(long courseId) { + return findWithEagerLearningPathsAndCompetenciesById(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId)); } /** diff --git a/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java index 270b84e66515..262a40536eff 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java @@ -2,7 +2,6 @@ import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.LOAD; -import java.util.List; import java.util.Optional; import java.util.Set; @@ -22,7 +21,7 @@ @Repository public interface LearningPathRepository extends JpaRepository { - List findByCourseIdAndUserId(long courseId, long userId); + Optional findByCourseIdAndUserId(long courseId, long userId); @EntityGraph(type = LOAD, attributePaths = { "competencies" }) Optional findWithEagerCompetenciesByCourseIdAndUserId(long courseId, long userId); diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index 7ffa552a90e2..8f2de854afcd 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -46,9 +46,7 @@ public LearningPathService(UserRepository userRepository, LearningPathRepository */ public void generateLearningPaths(@NotNull Course course) { var students = userRepository.getStudents(course); - students.forEach((student) -> { - generateLearningPathForUser(course, student); - }); + students.forEach((student) -> generateLearningPathForUser(course, student)); log.debug("Successfully created learning paths for all {} students in course (id={})", students.size(), course.getId()); } @@ -59,8 +57,8 @@ public void generateLearningPaths(@NotNull Course course) { * @param user student for which the learning path is generated */ public void generateLearningPathForUser(@NotNull Course course, @NotNull User user) { - var existingLearningPaths = learningPathRepository.findByCourseIdAndUserId(course.getId(), user.getId()); - if (existingLearningPaths == null || existingLearningPaths.isEmpty()) { + var existingLearningPath = learningPathRepository.findByCourseIdAndUserId(course.getId(), user.getId()); + if (existingLearningPath.isEmpty()) { LearningPath lpToCreate = new LearningPath(); lpToCreate.setUser(user); lpToCreate.setCourse(course); @@ -90,11 +88,9 @@ public SearchResultPageDTO getAllOfCourseOnPageWithSize(final Page * @param courseId course id that the learning paths belong to */ public void linkCompetencyToLearningPathsOfCourse(Competency competency, long courseId) { - var course = courseRepository.findWithEagerLearningPathsByIdElseThrow(courseId); + var course = courseRepository.findWithEagerLearningPathsAndCompetenciesByIdElseThrow(courseId); var learningPaths = course.getLearningPaths(); - learningPaths.forEach(learningPath -> { - learningPath.addCompetency(competency); - }); + learningPaths.forEach(learningPath -> learningPath.addCompetency(competency)); learningPathRepository.saveAll(learningPaths); log.debug("Linked competency (id={}) to learning paths", competency.getId()); } @@ -106,11 +102,9 @@ public void linkCompetencyToLearningPathsOfCourse(Competency competency, long co * @param courseId course id that the learning paths belong to */ public void removeLinkedCompetencyFromLearningPathsOfCourse(Competency competency, long courseId) { - var course = courseRepository.findWithEagerLearningPathsByIdElseThrow(courseId); + var course = courseRepository.findWithEagerLearningPathsAndCompetenciesByIdElseThrow(courseId); var learningPaths = course.getLearningPaths(); - learningPaths.forEach(learningPath -> { - learningPath.removeCompetency(competency); - }); + learningPaths.forEach(learningPath -> learningPath.removeCompetency(competency)); learningPathRepository.saveAll(learningPaths); log.debug("Removed linked competency (id={}) from learning paths", competency.getId()); } From 5941188f58d10a4680c0f77f698a73f2e4b249ab Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Wed, 28 Jun 2023 13:13:58 +0200 Subject: [PATCH 024/215] add client model --- .../webapp/app/entities/learning-path.model.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/main/webapp/app/entities/learning-path.model.ts diff --git a/src/main/webapp/app/entities/learning-path.model.ts b/src/main/webapp/app/entities/learning-path.model.ts new file mode 100644 index 000000000000..3c8646260f0d --- /dev/null +++ b/src/main/webapp/app/entities/learning-path.model.ts @@ -0,0 +1,14 @@ +import { BaseEntity } from 'app/shared/model/base-entity'; +import { Course } from 'app/entities/course.model'; +import { User } from 'app/core/user/user.model'; +import { Competency } from 'app/entities/competency.model'; + +export class LearningPath implements BaseEntity { + public id?: number; + public masteredCompetencies?: number; + public user?: User; + public course?: Course; + public competencies?: Competency[]; + + constructor() {} +} From a2d5f47f63d5381d59b26a46739a9c08b1c7b778 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Wed, 28 Jun 2023 13:14:43 +0200 Subject: [PATCH 025/215] rename service --- .../{learningPathService.service.ts => learning-path.service.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/webapp/app/course/learning-paths/{learningPathService.service.ts => learning-path.service.ts} (100%) diff --git a/src/main/webapp/app/course/learning-paths/learningPathService.service.ts b/src/main/webapp/app/course/learning-paths/learning-path.service.ts similarity index 100% rename from src/main/webapp/app/course/learning-paths/learningPathService.service.ts rename to src/main/webapp/app/course/learning-paths/learning-path.service.ts From dc991a2b77e1bf81c56cc116183a1bb6c3f00b04 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Wed, 28 Jun 2023 13:15:04 +0200 Subject: [PATCH 026/215] fix import --- .../learning-path-management.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts index 7a776e137c67..414012814840 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts @@ -3,7 +3,7 @@ import { ActivatedRoute } from '@angular/router'; import { CourseManagementService } from 'app/course/manage/course-management.service'; import { Course } from 'app/entities/course.model'; import { Subscription } from 'rxjs'; -import { LearningPathService } from 'app/course/learning-paths/learningPathService.service'; +import { LearningPathService } from 'app/course/learning-paths/learning-path.service'; import { finalize } from 'rxjs/operators'; import { HttpErrorResponse } from '@angular/common/http'; import { onError } from 'app/shared/util/global.utils'; From 0bac2e91907f42b5c3831680ff89d05ae70893cc Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Wed, 28 Jun 2023 13:15:17 +0200 Subject: [PATCH 027/215] add paging service --- .../learning-path-paging.service.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/main/webapp/app/course/learning-paths/learning-path-paging.service.ts diff --git a/src/main/webapp/app/course/learning-paths/learning-path-paging.service.ts b/src/main/webapp/app/course/learning-paths/learning-path-paging.service.ts new file mode 100644 index 000000000000..9b91d9922c1e --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/learning-path-paging.service.ts @@ -0,0 +1,24 @@ +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { PagingService } from 'app/exercises/shared/manage/paging.service'; +import { PageableSearch, SearchResult } from 'app/shared/table/pageable-table'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { LearningPath } from 'app/entities/learning-path.model'; + +type EntityResponseType = SearchResult; +@Injectable({ providedIn: 'root' }) +export class LearningPathPagingService extends PagingService { + public resourceUrl = 'api'; + + constructor(private http: HttpClient) { + super(); + } + + searchForLearningPaths(pageable: PageableSearch, courseId: number): Observable { + const params = this.createHttpParams(pageable); + return this.http + .get(`${this.resourceUrl}/courses/${courseId}/learning-paths`, { params, observe: 'response' }) + .pipe(map((resp: HttpResponse) => resp && resp.body!)); + } +} From ffabcf7055c3a0a580a7de225835d12770fa8308 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Wed, 28 Jun 2023 14:33:02 +0200 Subject: [PATCH 028/215] add draft for learning path pagable table --- .../learning-path-management.component.html | 53 ++++++- .../learning-path-management.component.ts | 129 +++++++++++++++++- src/main/webapp/i18n/de/competency.json | 9 +- src/main/webapp/i18n/en/competency.json | 9 +- 4 files changed, 195 insertions(+), 5 deletions(-) diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html index 7f1c73d0bb67..50330d87ffb3 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html @@ -21,6 +21,57 @@

Learning Pa

- +
+ Search for Learning Path: + + Loading... +
+ + + + + + + + + + + + + + + + + + + +
+ # + + + Name + + + Login + + + Progress + +
+ {{ learningPath.id }} + + + + + + {{ learningPath.masteredCompetencies }} + + +
+ +
+ + +
diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts index 414012814840..b89c29944c61 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts @@ -2,12 +2,24 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { CourseManagementService } from 'app/course/manage/course-management.service'; import { Course } from 'app/entities/course.model'; -import { Subscription } from 'rxjs'; +import { Subject, Subscription } from 'rxjs'; import { LearningPathService } from 'app/course/learning-paths/learning-path.service'; -import { finalize } from 'rxjs/operators'; +import { debounceTime, finalize, switchMap, tap } from 'rxjs/operators'; import { HttpErrorResponse } from '@angular/common/http'; import { onError } from 'app/shared/util/global.utils'; import { AlertService } from 'app/core/util/alert.service'; +import { PageableSearch, SearchResult, SortingOrder } from 'app/shared/table/pageable-table'; +import { LearningPathPagingService } from 'app/course/learning-paths/learning-path-paging.service'; +import { SortService } from 'app/shared/service/sort.service'; +import { LearningPath } from 'app/entities/learning-path.model'; +import { faSort } from '@fortawesome/free-solid-svg-icons'; + +export enum TableColumn { + ID = 'ID', + USER_NAME = 'USER_NAME', + USER_LOGIN = 'USER_LOGIN', + PROGRESS = 'PROGRESS', +} @Component({ selector: 'jhi-learning-path-management', @@ -21,13 +33,83 @@ export class LearningPathManagementComponent implements OnInit, OnDestroy { courseSub: Subscription; + searchLoading = false; + readonly column = TableColumn; + state: PageableSearch = { + page: 1, + pageSize: 50, + searchTerm: '', + sortingOrder: SortingOrder.ASCENDING, + sortedColumn: TableColumn.ID, + }; + content: SearchResult; + total = 0; + + private search = new Subject(); + private sort = new Subject(); + + // icons + faSort = faSort; + constructor( private activatedRoute: ActivatedRoute, private courseManagementService: CourseManagementService, private learningPathService: LearningPathService, private alertService: AlertService, + private pagingService: LearningPathPagingService, + private sortService: SortService, ) {} + get page(): number { + return this.state.page; + } + + set page(page: number) { + this.setSearchParam({ page }); + } + + get listSorting(): boolean { + return this.state.sortingOrder === SortingOrder.ASCENDING; + } + + /** + * Set the list sorting direction + * + * @param ascending {boolean} Ascending order set + */ + set listSorting(ascending: boolean) { + const sortingOrder = ascending ? SortingOrder.ASCENDING : SortingOrder.DESCENDING; + this.setSearchParam({ sortingOrder }); + } + + /** + * Gives the ID for any item in the table, so that it can be tracked/identified by ngFor + * + * @param index The index of the element in the ngFor + * @param item The item itself + * @returns The ID of the item + */ + trackId(index: number, item: LearningPath): number { + return item.id!; + } + + get sortedColumn(): string { + return this.state.sortedColumn; + } + + set sortedColumn(sortedColumn: string) { + this.setSearchParam({ sortedColumn }); + } + + get searchTerm(): string { + return this.state.searchTerm; + } + + set searchTerm(searchTerm: string) { + this.state.searchTerm = searchTerm; + this.search.next(); + } + ngOnInit(): void { this.activatedRoute.parent!.params.subscribe((params) => { this.courseId = +params['courseId']; @@ -73,4 +155,47 @@ export class LearningPathManagementComponent implements OnInit, OnDestroy { error: (res: HttpErrorResponse) => onError(this.alertService, res), }); } + + /** + * Method to perform the search based on a search subject + * + * @param searchSubject The search subject which we use to search. + * @param debounce The delay we apply to delay the feedback / wait for input + */ + performSearch(searchSubject: Subject, debounce: number): void { + searchSubject + .pipe( + debounceTime(debounce), + tap(() => (this.searchLoading = true)), + switchMap(() => this.pagingService.searchForLearningPaths(this.state, this.courseId)), + ) + .subscribe((resp) => { + this.content = resp; + this.searchLoading = false; + this.total = resp.numberOfPages * this.state.pageSize; + }); + } + + sortRows() { + this.sortService.sortByProperty(this.content.resultsOnPage, this.sortedColumn, this.listSorting); + } + + private setSearchParam(patch: Partial): void { + Object.assign(this.state, patch); + this.sort.next(); + } + + /** + * Callback function when the user navigates through the page results + * + * @param pageNumber The current page number + */ + onPageChange(pageNumber: number) { + if (pageNumber) { + this.page = pageNumber; + } + } + viewLearningPath(learningPath: LearningPath) { + // TODO + } } diff --git a/src/main/webapp/i18n/de/competency.json b/src/main/webapp/i18n/de/competency.json index 258f89ba8054..00b58ef1c3a8 100644 --- a/src/main/webapp/i18n/de/competency.json +++ b/src/main/webapp/i18n/de/competency.json @@ -149,7 +149,14 @@ "title": "Lernpfadmanagement", "isDisabled": "Lernpfade sind für diesen Kurs nicht aktiviert.", "enable": "Lernpfade aktivieren", - "enableHint": "Die Erstellung von Lernppfaden für alle Studierende kann einige Minuten dauern." + "enableHint": "Die Erstellung von Lernpfaden für alle Studierende kann einige Minuten dauern.", + "search": "Suche nach Lernpfaden:", + "table": { + "name": "Name", + "login": "Login", + "progress": "Fortschritt", + "view": "Ansehen" + } } } } diff --git a/src/main/webapp/i18n/en/competency.json b/src/main/webapp/i18n/en/competency.json index 815b730ca32c..f73df4090488 100644 --- a/src/main/webapp/i18n/en/competency.json +++ b/src/main/webapp/i18n/en/competency.json @@ -148,7 +148,14 @@ "title": "Learning Path Management", "isDisabled": "Learning Paths are currently disabled for this course.", "enable": "Enable Learning Paths", - "enableHint": "The creation of Learning Paths for every student may take a few minutes." + "enableHint": "The creation of Learning Paths for every student may take a few minutes.", + "search": "Search for Learning Path:", + "table": { + "name": "Name", + "login": "Login", + "progress": "Progress", + "view": "View" + } } } } From 2681ec0f0d2f92635f2487524bd867e98336648c Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Wed, 28 Jun 2023 15:33:46 +0200 Subject: [PATCH 029/215] add more server tests --- .../lecture/LearningPathIntegrationTest.java | 117 +++++++++++++++++- 1 file changed, 111 insertions(+), 6 deletions(-) diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java index 06045dcf5303..b39a0e87d0c8 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java @@ -22,6 +22,7 @@ import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.competency.LearningPath; import de.tum.in.www1.artemis.repository.CourseRepository; +import de.tum.in.www1.artemis.repository.LearningPathRepository; import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.service.LearningPathService; import de.tum.in.www1.artemis.user.UserUtilService; @@ -52,6 +53,9 @@ class LearningPathIntegrationTest extends AbstractSpringIntegrationBambooBitbuck @Autowired PageableSearchUtilService pageableSearchUtilService; + @Autowired + LearningPathRepository learningPathRepository; + private Course course; private Competency[] competencies; @@ -82,12 +86,12 @@ void setupTestScenario() { course = courseUtilService.createCourse(); competencies = competencyUtilService.createCompetencies(course, 5); - } private void enableLearningPathsForTestingCourse() { - course.setLeanringPathsEnabled(true); + course = courseRepository.findWithEagerLearningPathsAndCompetenciesByIdElseThrow(course.getId()); learningPathService.generateLearningPaths(course); + course.setLeanringPathsEnabled(true); course = courseRepository.save(course); } @@ -109,6 +113,19 @@ private void testAllPreAuthorize() throws Exception { request.getSearchResult("/api/courses/" + course.getId() + "/learning-paths", HttpStatus.FORBIDDEN, LearningPath.class, pageableSearchUtilService.searchMapping(search)); } + private Competency createCompetencyRESTCall() throws Exception { + final var competencyToCreate = new Competency(); + competencyToCreate.setTitle("CompetencyToCreateTitle"); + competencyToCreate.setCourse(course); + return request.postWithResponseBody("/api/courses/" + course.getId() + "/competencies", competencyToCreate, Competency.class, HttpStatus.CREATED); + } + + private Competency importCompetencyRESTCall() throws Exception { + final var course2 = courseUtilService.createCourse(); + final var competencyToImport = competencyUtilService.createCompetency(course2); + return request.postWithResponseBody("/api/courses/" + course.getId() + "/competencies/import", competencyToImport, Competency.class, HttpStatus.CREATED); + } + @Test @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") void testAll_asStudent() throws Exception { @@ -131,10 +148,12 @@ void testAll_asEditor() throws Exception { @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") void testEnableLearningPaths() throws Exception { request.putWithResponseBody("/api/courses/" + course.getId() + "/learning-paths/enable", course, Course.class, HttpStatus.OK); - final var updatedCourse = courseRepository.findWithEagerLearningPathsByIdElseThrow(course.getId()); - assertThat(updatedCourse.getLearningPathsEnabled()).isTrue().as("should enable LearningPaths"); + final var updatedCourse = courseRepository.findWithEagerLearningPathsAndCompetenciesByIdElseThrow(course.getId()); + assertThat(updatedCourse.getLearningPathsEnabled()).as("should enable LearningPaths").isTrue(); assertThat(updatedCourse.getLearningPaths()).isNotNull(); - assertThat(updatedCourse.getLearningPaths().size()).isEqualTo(NUMBER_OF_STUDENTS).as("should create LearningPath for each student"); + assertThat(updatedCourse.getLearningPaths().size()).as("should create LearningPath for each student").isEqualTo(NUMBER_OF_STUDENTS); + updatedCourse.getLearningPaths().forEach( + lp -> assertThat(lp.getCompetencies().size()).as("LearningPath (id={}) should have be linked to all Competencies", lp.getId()).isEqualTo(competencies.length)); } @Test @@ -159,7 +178,7 @@ void testGenerateLearningPathOnEnrollment() throws Exception { final var updatedUser = request.postWithResponseBody("/api/courses/" + course.getId() + "/enroll", null, User.class, HttpStatus.OK); final var updatedUserWithLearningPaths = userRepository.findWithLearningPathsByIdElseThrow(updatedUser.getId()); assertThat(updatedUserWithLearningPaths.getLearningPaths()).isNotNull(); - assertThat(updatedUserWithLearningPaths.getLearningPaths().size()).isEqualTo(1).as("should create LearningPath for student"); + assertThat(updatedUserWithLearningPaths.getLearningPaths().size()).as("should create LearningPath for student").isEqualTo(1); } private void setupEnrollmentRequestMocks() throws JsonProcessingException, URISyntaxException { @@ -196,4 +215,90 @@ void testGetLearningPathsOnPageForCourseExactlyStudent() throws Exception { pageableSearchUtilService.searchMapping(search)); assertThat(result.getResultsOnPage()).hasSize(1); } + + @Test + @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") + void testAddCompetencyToLearningPathsOnCreateCompetency() throws Exception { + enableLearningPathsForTestingCourse(); + + final var createdCompetency = createCompetencyRESTCall(); + + final var student = userRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var learningPathOptional = learningPathRepository.findWithEagerCompetenciesByCourseIdAndUserId(course.getId(), student.getId()); + assertThat(learningPathOptional).isPresent(); + assertThat(learningPathOptional.get().getCompetencies()).as("should contain new competency").contains(createdCompetency); + assertThat(learningPathOptional.get().getCompetencies().size()).as("should not remove old competencies").isEqualTo(competencies.length + 1); + final var oldCompetencies = Set.of(competencies[0], competencies[1], competencies[2], competencies[3], competencies[4]); + assertThat(learningPathOptional.get().getCompetencies()).as("should not remove old competencies").containsAll(oldCompetencies); + } + + @Test + @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") + void testAddCompetencyToLearningPathsOnImportCompetency() throws Exception { + enableLearningPathsForTestingCourse(); + + final var importedCompetency = importCompetencyRESTCall(); + + final var student = userRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var learningPathOptional = learningPathRepository.findWithEagerCompetenciesByCourseIdAndUserId(course.getId(), student.getId()); + assertThat(learningPathOptional).isPresent(); + assertThat(learningPathOptional.get().getCompetencies()).as("should contain new competency").contains(importedCompetency); + assertThat(learningPathOptional.get().getCompetencies().size()).as("should not remove old competencies").isEqualTo(competencies.length + 1); + final var oldCompetencies = Set.of(competencies[0], competencies[1], competencies[2], competencies[3], competencies[4]); + assertThat(learningPathOptional.get().getCompetencies()).as("should not remove old competencies").containsAll(oldCompetencies); + } + + @Test + @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") + void testRemoveCompetencyFromLearningPathsOnDeleteCompetency() throws Exception { + enableLearningPathsForTestingCourse(); + + request.delete("/api/courses/" + course.getId() + "/competencies/" + competencies[0].getId(), HttpStatus.OK); + + final var student = userRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var learningPathOptional = learningPathRepository.findWithEagerCompetenciesByCourseIdAndUserId(course.getId(), student.getId()); + assertThat(learningPathOptional).isPresent(); + assertThat(learningPathOptional.get().getCompetencies()).as("should not contain deleted competency").doesNotContain(competencies[0]); + final var nonDeletedCompetencies = Set.of(competencies[1], competencies[2], competencies[3], competencies[4]); + assertThat(learningPathOptional.get().getCompetencies().size()).as("should contain competencies that have not been deleted").isEqualTo(nonDeletedCompetencies.size()); + assertThat(learningPathOptional.get().getCompetencies()).as("should contain competencies that have not been deleted").containsAll(nonDeletedCompetencies); + } + + @Test + @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") + void testUpdateLearningPathProgressOnCreateCompetency() throws Exception { + enableLearningPathsForTestingCourse(); + final var createdCompetency = createCompetencyRESTCall(); + // TODO + } + + @Test + @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") + void testUpdateLearningPathProgressOnUpdateCompetency() throws Exception { + enableLearningPathsForTestingCourse(); + // TODO + } + + @Test + @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") + void testUpdateLearningPathProgressOnImportCompetency() throws Exception { + enableLearningPathsForTestingCourse(); + final var importedCompetency = importCompetencyRESTCall(); + // TODO + } + + @Test + @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") + void testUpdateLearningPathProgressOnDeleteCompetency() throws Exception { + enableLearningPathsForTestingCourse(); + request.delete("/api/courses/" + course.getId() + "/competencies/" + competencies[0].getId(), HttpStatus.OK); + // TODO + } + + @Test + @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") + void testUpdateLearningPathProgress() { + enableLearningPathsForTestingCourse(); + // TODO + } } From 72c6c00a382e610a7ac96036d68b041e32a600fc Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 29 Jun 2023 11:09:31 +0200 Subject: [PATCH 030/215] improve tests --- .../lecture/LearningPathIntegrationTest.java | 64 +++++++++++++++---- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java index b39a0e87d0c8..a3ee3f1a45c2 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java @@ -4,6 +4,7 @@ import java.net.URISyntaxException; import java.time.ZonedDateTime; +import java.util.List; import java.util.Set; import org.junit.jupiter.api.BeforeEach; @@ -17,13 +18,13 @@ import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; import de.tum.in.www1.artemis.competency.CompetencyUtilService; import de.tum.in.www1.artemis.course.CourseUtilService; -import de.tum.in.www1.artemis.domain.Course; -import de.tum.in.www1.artemis.domain.User; +import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.competency.LearningPath; -import de.tum.in.www1.artemis.repository.CourseRepository; -import de.tum.in.www1.artemis.repository.LearningPathRepository; -import de.tum.in.www1.artemis.repository.UserRepository; +import de.tum.in.www1.artemis.exercise.ExerciseUtilService; +import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.participation.ParticipationUtilService; +import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.service.LearningPathService; import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.util.PageableSearchUtilService; @@ -56,10 +57,30 @@ class LearningPathIntegrationTest extends AbstractSpringIntegrationBambooBitbuck @Autowired LearningPathRepository learningPathRepository; + @Autowired + ExerciseUtilService exerciseUtilService; + + @Autowired + TextExerciseUtilService textExerciseUtilService; + + @Autowired + ParticipationUtilService participationUtilService; + + @Autowired + LectureRepository lectureRepository; + + @Autowired + LectureUtilService lectureUtilService; + + @Autowired + GradingCriterionRepository gradingCriterionRepository; + private Course course; private Competency[] competencies; + private TextExercise textExercise; + private final int NUMBER_OF_STUDENTS = 5; private final String STUDENT_OF_COURSE = TEST_PREFIX + "student1"; @@ -73,7 +94,7 @@ class LearningPathIntegrationTest extends AbstractSpringIntegrationBambooBitbuck private User studentNotInCourse; @BeforeEach - void setupTestScenario() { + void setupTestScenario() throws Exception { participantScoreScheduleService.activate(); userUtilService.addUsers(TEST_PREFIX, NUMBER_OF_STUDENTS, 1, 1, 1); @@ -82,10 +103,22 @@ void setupTestScenario() { studentNotInCourse = userUtilService.createAndSaveUser(TEST_PREFIX + "student1337"); userUtilService.createAndSaveUser(TEST_PREFIX + "instructor1337"); - // creating course - course = courseUtilService.createCourse(); - + course = courseUtilService.createCoursesWithExercisesAndLectures(TEST_PREFIX, true, true, 1).get(0); competencies = competencyUtilService.createCompetencies(course, 5); + + textExercise = textExerciseUtilService.createIndividualTextExercise(course, past(1), future(1), future(2)); + List gradingCriteria = exerciseUtilService.addGradingInstructionsToExercise(textExercise); + gradingCriterionRepository.saveAll(gradingCriteria); + participationUtilService.addAssessmentWithFeedbackWithGradingInstructionsForExercise(textExercise, STUDENT_OF_COURSE); + + Lecture lecture = new Lecture(); + lecture.setDescription("Test Lecture"); + lecture.setCourse(course); + lectureRepository.save(lecture); + + final var textUnit = lectureUtilService.createTextUnit(); + lectureUtilService.addLectureUnitsToLecture(lecture, List.of(textUnit)); + } private void enableLearningPathsForTestingCourse() { @@ -126,6 +159,15 @@ private Competency importCompetencyRESTCall() throws Exception { return request.postWithResponseBody("/api/courses/" + course.getId() + "/competencies/import", competencyToImport, Competency.class, HttpStatus.CREATED); } + private Competency updateCompetencyRESTCall() throws Exception { + competencies[0].setTitle("Updated Title"); + return request.putWithResponseBody("/api/courses/" + course.getId() + "/competencies", competencies[0], Competency.class, HttpStatus.OK); + } + + private void deleteCompetencyRESTCall(Competency competency) throws Exception { + request.delete("/api/courses/" + course.getId() + "/competencies/" + competency.getId(), HttpStatus.OK); + } + @Test @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") void testAll_asStudent() throws Exception { @@ -253,7 +295,7 @@ void testAddCompetencyToLearningPathsOnImportCompetency() throws Exception { void testRemoveCompetencyFromLearningPathsOnDeleteCompetency() throws Exception { enableLearningPathsForTestingCourse(); - request.delete("/api/courses/" + course.getId() + "/competencies/" + competencies[0].getId(), HttpStatus.OK); + deleteCompetencyRESTCall(competencies[0]); final var student = userRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); final var learningPathOptional = learningPathRepository.findWithEagerCompetenciesByCourseIdAndUserId(course.getId(), student.getId()); @@ -291,7 +333,7 @@ void testUpdateLearningPathProgressOnImportCompetency() throws Exception { @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") void testUpdateLearningPathProgressOnDeleteCompetency() throws Exception { enableLearningPathsForTestingCourse(); - request.delete("/api/courses/" + course.getId() + "/competencies/" + competencies[0].getId(), HttpStatus.OK); + deleteCompetencyRESTCall(competencies[0]); // TODO } From 04763bb1d252b7f164e853fc168c9c63c457a734 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 29 Jun 2023 12:40:20 +0200 Subject: [PATCH 031/215] rename progress --- .../www1/artemis/domain/competency/LearningPath.java | 12 ++++++------ .../in/www1/artemis/service/LearningPathService.java | 6 +++--- .../liquibase/changelog/20230622000000_changelog.xml | 2 +- .../learning-path-management.component.html | 2 +- src/main/webapp/app/entities/learning-path.model.ts | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java b/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java index 18de919a47f0..f6a0bb270386 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java @@ -20,8 +20,8 @@ @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) public class LearningPath extends DomainObject { - @Column(name = "mastered_competencies") - private int masteredCompetencies; + @Column(name = "progress") + private int progress; @ManyToOne @JoinColumn(name = "user_id") @@ -38,12 +38,12 @@ public class LearningPath extends DomainObject { @JsonIgnoreProperties({ "exercises", "course", "learningPaths" }) private Set competencies = new HashSet<>(); - public int getMasteredCompetencies() { - return masteredCompetencies; + public int getProgress() { + return progress; } - public void setMasteredCompetencies(int masteredCompetencies) { - this.masteredCompetencies = masteredCompetencies; + public void setProgress(int progress) { + this.progress = progress; } public User getUser() { diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index 8f2de854afcd..ab26eac1eabf 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -122,11 +122,11 @@ public void updateLearningPathProgress(final long courseId, final long userId) { private void updateLearningPathProgress(final LearningPath learningPath) { final var userId = learningPath.getUser().getId(); final var competencyIds = learningPath.getCompetencies().stream().map(Competency::getId).collect(Collectors.toSet()); - final var progress = competencyProgressRepository.findAllByCompetencyIdsAndUserId(competencyIds, userId); + final var competencyProgresses = competencyProgressRepository.findAllByCompetencyIdsAndUserId(competencyIds, userId); // TODO: consider optional competencies - final var mastered = (int) progress.stream().filter(CompetencyProgressService::isMastered).count(); - learningPath.setMasteredCompetencies(mastered); + final var completed = (float) competencyProgresses.stream().filter(CompetencyProgressService::isMastered).count(); + learningPath.setProgress(Math.round(completed / (float) learningPath.getCompetencies().size())); learningPathRepository.save(learningPath); log.debug("Updated LearningPath (id={}) for user (id={})", learningPath.getId(), userId); } diff --git a/src/main/resources/config/liquibase/changelog/20230622000000_changelog.xml b/src/main/resources/config/liquibase/changelog/20230622000000_changelog.xml index 794742dae9c4..d81985f545dc 100644 --- a/src/main/resources/config/liquibase/changelog/20230622000000_changelog.xml +++ b/src/main/resources/config/liquibase/changelog/20230622000000_changelog.xml @@ -14,7 +14,7 @@ - + diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html index 50330d87ffb3..b53219e7bc09 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html @@ -60,7 +60,7 @@

Learning Pa - {{ learningPath.masteredCompetencies }} + diff --git a/src/main/webapp/app/entities/learning-path.model.ts b/src/main/webapp/app/entities/learning-path.model.ts index 3c8646260f0d..b36aac4a5f73 100644 --- a/src/main/webapp/app/entities/learning-path.model.ts +++ b/src/main/webapp/app/entities/learning-path.model.ts @@ -5,7 +5,7 @@ import { Competency } from 'app/entities/competency.model'; export class LearningPath implements BaseEntity { public id?: number; - public masteredCompetencies?: number; + public progress?: number; public user?: User; public course?: Course; public competencies?: Competency[]; From f6f4a0dacf470d806739d6b77577c47cf72c628f Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 29 Jun 2023 15:33:13 +0200 Subject: [PATCH 032/215] learning path service and test --- .../learning-paths/learning-path.service.ts | 3 +-- .../service/learning-path.service.spec.ts | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 src/test/javascript/spec/service/learning-path.service.spec.ts diff --git a/src/main/webapp/app/course/learning-paths/learning-path.service.ts b/src/main/webapp/app/course/learning-paths/learning-path.service.ts index fa64567c62ee..5cf49c2c8115 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path.service.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path.service.ts @@ -10,7 +10,6 @@ export class LearningPathService { constructor(private httpClient: HttpClient) {} enableLearningPaths(courseId: number): Observable> { - // return this.httpClient.put(`${this.resourceURL}/courses/${courseId}/learning-paths/enable`, { observe: 'response' }) - return new Observable(); + return this.httpClient.put(`${this.resourceURL}/courses/${courseId}/learning-paths/enable`, null, { observe: 'response' }); } } diff --git a/src/test/javascript/spec/service/learning-path.service.spec.ts b/src/test/javascript/spec/service/learning-path.service.spec.ts new file mode 100644 index 000000000000..1d46f57ce2ae --- /dev/null +++ b/src/test/javascript/spec/service/learning-path.service.spec.ts @@ -0,0 +1,24 @@ +import { MockHttpService } from '../helpers/mocks/service/mock-http.service'; +import { LearningPathService } from 'app/course/learning-paths/learning-path.service'; + +describe('LearningPathService', () => { + let learningPathService: LearningPathService; + let httpService: MockHttpService; + let putStub: jest.SpyInstance; + + beforeEach(() => { + httpService = new MockHttpService(); + learningPathService = new LearningPathService(httpService); + putStub = jest.spyOn(httpService, 'put'); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should send a request to the server to activate the user', () => { + learningPathService.enableLearningPaths(1).subscribe(); + expect(putStub).toHaveBeenCalledOnce(); + expect(putStub).toHaveBeenCalledWith('api/courses/1/learning-paths/enable', null, { observe: 'response' }); + }); +}); From 3c6d997871aff777b008206607f459ffb4b790e9 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 29 Jun 2023 15:50:16 +0200 Subject: [PATCH 033/215] add test for paging service --- .../learning-path-paging.service.spec.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/test/javascript/spec/service/learning-path-paging.service.spec.ts diff --git a/src/test/javascript/spec/service/learning-path-paging.service.spec.ts b/src/test/javascript/spec/service/learning-path-paging.service.spec.ts new file mode 100644 index 000000000000..15acf56fc044 --- /dev/null +++ b/src/test/javascript/spec/service/learning-path-paging.service.spec.ts @@ -0,0 +1,40 @@ +import { MockHttpService } from '../helpers/mocks/service/mock-http.service'; +import { LearningPathPagingService } from 'app/course/learning-paths/learning-path-paging.service'; +import { PageableSearch, SortingOrder } from 'app/shared/table/pageable-table'; +import { HttpParams } from '@angular/common/http'; +import { TableColumn } from 'app/course/learning-paths/learning-path-management/learning-path-management.component'; + +describe('LearningPathPagingService', () => { + let learningPathPagingService: LearningPathPagingService; + let httpService: MockHttpService; + let getStub: jest.SpyInstance; + + beforeEach(() => { + httpService = new MockHttpService(); + learningPathPagingService = new LearningPathPagingService(httpService); + getStub = jest.spyOn(httpService, 'get'); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should send a request to the server to activate the user', () => { + const pageable = { + page: 1, + pageSize: 10, + searchTerm: 'initialSearchTerm', + sortingOrder: SortingOrder.DESCENDING, + sortedColumn: TableColumn.ID, + } as PageableSearch; + learningPathPagingService.searchForLearningPaths(pageable, 1).subscribe(); + const params = new HttpParams() + .set('pageSize', String(pageable.pageSize)) + .set('page', String(pageable.page)) + .set('sortingOrder', pageable.sortingOrder) + .set('searchTerm', pageable.searchTerm) + .set('sortedColumn', pageable.sortedColumn); + expect(getStub).toHaveBeenCalledOnce(); + expect(getStub).toHaveBeenCalledWith('api/courses/1/learning-paths', { params, observe: 'response' }); + }); +}); From 2aef85c135bd0c9262ef59e824d8c7f96c6fb518 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 29 Jun 2023 15:51:49 +0200 Subject: [PATCH 034/215] allow loading of learning paths --- .../learning-path-management.component.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts index b89c29944c61..430a3639fe95 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts @@ -127,7 +127,10 @@ export class LearningPathManagementComponent implements OnInit, OnDestroy { }); if (this.course?.learningPathEnabled) { - // TODO: load learning paths of students + this.content = { resultsOnPage: [], numberOfPages: 0 }; + + this.performSearch(this.sort, 0); + this.performSearch(this.search, 300); } this.isLoading = false; From 7ae59ff88b1e643a4b823efeef154d54aeb99b4b Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 29 Jun 2023 15:52:07 +0200 Subject: [PATCH 035/215] add tests for management component --- ...learning-path-management.component.spec.ts | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 src/test/javascript/spec/component/learning-paths/learning-path-management.component.spec.ts diff --git a/src/test/javascript/spec/component/learning-paths/learning-path-management.component.spec.ts b/src/test/javascript/spec/component/learning-paths/learning-path-management.component.spec.ts new file mode 100644 index 000000000000..d661106eb0a2 --- /dev/null +++ b/src/test/javascript/spec/component/learning-paths/learning-path-management.component.spec.ts @@ -0,0 +1,126 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { LearningPathManagementComponent, TableColumn } from 'app/course/learning-paths/learning-path-management/learning-path-management.component'; +import { LearningPathPagingService } from 'app/course/learning-paths/learning-path-paging.service'; +import { SortService } from 'app/shared/service/sort.service'; +import { PageableSearch, SearchResult, SortingOrder } from 'app/shared/table/pageable-table'; +import { LearningPath } from 'app/entities/learning-path.model'; +import { ArtemisTestModule } from '../../test.module'; +import { MockComponent, MockDirective } from 'ng-mocks'; +import { ButtonComponent } from 'app/shared/components/button.component'; +import { NgbPagination } from '@ng-bootstrap/ng-bootstrap'; +import { SortByDirective } from 'app/shared/sort/sort-by.directive'; +import { SortDirective } from 'app/shared/sort/sort.directive'; +import { of } from 'rxjs'; +import { CourseManagementService } from 'app/course/manage/course-management.service'; +import { Course } from 'app/entities/course.model'; + +describe('LearningPathManagementComponent', () => { + let fixture: ComponentFixture; + let comp: LearningPathManagementComponent; + let courseManagementService: CourseManagementService; + let findCourseStub: jest.SpyInstance; + let pagingService: LearningPathPagingService; + let sortService: SortService; + let searchForLearningPathsStub: jest.SpyInstance; + let sortByPropertyStub: jest.SpyInstance; + let searchResult: SearchResult; + let state: PageableSearch; + let learningPath: LearningPath; + let course: Course; + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule, MockComponent(NgbPagination)], + declarations: [LearningPathManagementComponent, MockComponent(ButtonComponent), MockDirective(SortByDirective), MockDirective(SortDirective)], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(LearningPathManagementComponent); + comp = fixture.componentInstance; + courseManagementService = TestBed.inject(CourseManagementService); + findCourseStub = jest.spyOn(courseManagementService, 'find'); + pagingService = TestBed.inject(LearningPathPagingService); + sortService = TestBed.inject(SortService); + searchForLearningPathsStub = jest.spyOn(pagingService, 'searchForLearningPaths'); + sortByPropertyStub = jest.spyOn(sortService, 'sortByProperty'); + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + beforeEach(() => { + fixture.detectChanges(); + course = new Course(); + course.learningPathEnabled = true; + findCourseStub.mockReturnValue(of(course)); + learningPath = new LearningPath(); + learningPath.id = 1; + searchResult = { numberOfPages: 3, resultsOnPage: [learningPath] }; + state = { + page: 1, + pageSize: 10, + searchTerm: 'initialSearchTerm', + sortingOrder: SortingOrder.DESCENDING, + sortedColumn: TableColumn.ID, + ...searchResult, + }; + searchForLearningPathsStub.mockReturnValue(of(searchResult)); + }); + + const setStateAndCallOnInit = (middleExpectation: () => void) => { + comp.state = { ...state }; + comp.ngOnInit(); + middleExpectation(); + expect(comp.content).toEqual(searchResult); + comp.sortRows(); + expect(sortByPropertyStub).toHaveBeenCalledWith(searchResult.resultsOnPage, comp.sortedColumn, comp.listSorting); + }; + + it('should set content to paging result on sort', fakeAsync(() => { + expect(comp.listSorting).toBeFalse(); + setStateAndCallOnInit(() => { + comp.listSorting = true; + tick(10); + expect(searchForLearningPathsStub).toHaveBeenCalledWith({ ...state, sortingOrder: SortingOrder.ASCENDING }); + expect(comp.listSorting).toBeTrue(); + }); + })); + + it('should set content to paging result on pageChange', fakeAsync(() => { + expect(comp.page).toBe(1); + setStateAndCallOnInit(() => { + comp.onPageChange(5); + tick(10); + expect(searchForLearningPathsStub).toHaveBeenCalledWith({ ...state, page: 5 }); + expect(comp.page).toBe(5); + }); + })); + + it('should set content to paging result on search', fakeAsync(() => { + expect(comp.searchTerm).toBe(''); + setStateAndCallOnInit(() => { + const givenSearchTerm = 'givenSearchTerm'; + comp.searchTerm = givenSearchTerm; + tick(10); + expect(searchForLearningPathsStub).not.toHaveBeenCalled(); + tick(290); + expect(searchForLearningPathsStub).toHaveBeenCalledWith({ ...state, searchTerm: givenSearchTerm }); + expect(comp.searchTerm).toEqual(givenSearchTerm); + }); + })); + + it('should set content to paging result on sortedColumn change', fakeAsync(() => { + expect(comp.sortedColumn).toEqual(TableColumn.ID); + setStateAndCallOnInit(() => { + comp.sortedColumn = TableColumn.USER_LOGIN; + tick(10); + expect(searchForLearningPathsStub).toHaveBeenCalledWith({ ...state, sortedColumn: TableColumn.USER_LOGIN }); + expect(comp.sortedColumn).toEqual(TableColumn.USER_LOGIN); + }); + })); + + it('should return competency id', () => { + expect(comp.trackId(0, learningPath)).toEqual(learningPath.id); + }); +}); From 883d3c72545ba24b6339bd2e4fd941738231daaa Mon Sep 17 00:00:00 2001 From: MaximilianAnzinger Date: Thu, 29 Jun 2023 18:28:44 +0200 Subject: [PATCH 036/215] test learning path update --- .../CompetencyProgressRepository.java | 2 +- .../artemis/service/LearningPathService.java | 2 +- .../lecture/LearningPathIntegrationTest.java | 57 ++++++++----------- 3 files changed, 27 insertions(+), 34 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/CompetencyProgressRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/CompetencyProgressRepository.java index 17227016cdb4..691a488d3f8c 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/CompetencyProgressRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/CompetencyProgressRepository.java @@ -56,7 +56,7 @@ public interface CompetencyProgressRepository extends JpaRepository findAllByCompetencyIdsAndUserId(@Param("competencyId") Set competencyIds, @Param("userId") Long userId); + List findAllByCompetencyIdsAndUserId(@Param("competencyIds") Set competencyIds, @Param("userId") Long userId); @Query(""" SELECT AVG(cp.confidence) diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index ab26eac1eabf..e144c9dea3e1 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -126,7 +126,7 @@ private void updateLearningPathProgress(final LearningPath learningPath) { // TODO: consider optional competencies final var completed = (float) competencyProgresses.stream().filter(CompetencyProgressService::isMastered).count(); - learningPath.setProgress(Math.round(completed / (float) learningPath.getCompetencies().size())); + learningPath.setProgress(Math.round(completed * 100 / (float) learningPath.getCompetencies().size())); learningPathRepository.save(learningPath); log.debug("Updated LearningPath (id={}) for user (id={})", learningPath.getId(), userId); } diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java index a3ee3f1a45c2..de8b35ff1679 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java @@ -21,11 +21,14 @@ import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.competency.LearningPath; +import de.tum.in.www1.artemis.domain.lecture.TextUnit; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.*; +import de.tum.in.www1.artemis.service.CompetencyProgressService; import de.tum.in.www1.artemis.service.LearningPathService; +import de.tum.in.www1.artemis.service.LectureUnitService; import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.util.PageableSearchUtilService; @@ -75,12 +78,20 @@ class LearningPathIntegrationTest extends AbstractSpringIntegrationBambooBitbuck @Autowired GradingCriterionRepository gradingCriterionRepository; + @Autowired + LectureUnitService lectureUnitService; + + @Autowired + CompetencyProgressService competencyProgressService; + private Course course; private Competency[] competencies; private TextExercise textExercise; + private TextUnit textUnit; + private final int NUMBER_OF_STUDENTS = 5; private final String STUDENT_OF_COURSE = TEST_PREFIX + "student1"; @@ -95,8 +106,6 @@ class LearningPathIntegrationTest extends AbstractSpringIntegrationBambooBitbuck @BeforeEach void setupTestScenario() throws Exception { - participantScoreScheduleService.activate(); - userUtilService.addUsers(TEST_PREFIX, NUMBER_OF_STUDENTS, 1, 1, 1); // Add users that are not in the course @@ -116,9 +125,11 @@ void setupTestScenario() throws Exception { lecture.setCourse(course); lectureRepository.save(lecture); - final var textUnit = lectureUtilService.createTextUnit(); + textUnit = lectureUtilService.createTextUnit(); lectureUtilService.addLectureUnitsToLecture(lecture, List.of(textUnit)); + final var student = userRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + lectureUnitService.setLectureUnitCompletion(textUnit, student, true); } private void enableLearningPathsForTestingCourse() { @@ -150,6 +161,7 @@ private Competency createCompetencyRESTCall() throws Exception { final var competencyToCreate = new Competency(); competencyToCreate.setTitle("CompetencyToCreateTitle"); competencyToCreate.setCourse(course); + competencyToCreate.setLectureUnits(Set.of(textUnit)); return request.postWithResponseBody("/api/courses/" + course.getId() + "/competencies", competencyToCreate, Competency.class, HttpStatus.CREATED); } @@ -308,39 +320,20 @@ void testRemoveCompetencyFromLearningPathsOnDeleteCompetency() throws Exception @Test @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") - void testUpdateLearningPathProgressOnCreateCompetency() throws Exception { + void testUpdateLearningPathProgress() throws Exception { enableLearningPathsForTestingCourse(); - final var createdCompetency = createCompetencyRESTCall(); - // TODO - } - @Test - @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") - void testUpdateLearningPathProgressOnUpdateCompetency() throws Exception { - enableLearningPathsForTestingCourse(); - // TODO - } + // add competency with completed learning unit + final var createdCompetency = createCompetencyRESTCall(); - @Test - @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") - void testUpdateLearningPathProgressOnImportCompetency() throws Exception { - enableLearningPathsForTestingCourse(); - final var importedCompetency = importCompetencyRESTCall(); - // TODO - } + final var student = userRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + var learningPath = learningPathRepository.findWithEagerCompetenciesByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); + assertThat(learningPath.getProgress()).as("contains no completed competency").isEqualTo(0); - @Test - @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") - void testUpdateLearningPathProgressOnDeleteCompetency() throws Exception { - enableLearningPathsForTestingCourse(); - deleteCompetencyRESTCall(competencies[0]); - // TODO - } + // force update to avoid waiting for scheduler + competencyProgressService.updateCompetencyProgress(createdCompetency.getId(), student); - @Test - @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") - void testUpdateLearningPathProgress() { - enableLearningPathsForTestingCourse(); - // TODO + learningPath = learningPathRepository.findWithEagerCompetenciesByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); + assertThat(learningPath.getProgress()).as("contains completed competency").isNotEqualTo(0); } } From 80f41643a84bccdb18212513f0da9d94209dfb61 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Fri, 30 Jun 2023 12:27:24 +0200 Subject: [PATCH 037/215] Update learning-path-management.component.ts --- .../learning-path-management.component.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts index 430a3639fe95..205ec27ba25c 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts @@ -155,6 +155,9 @@ export class LearningPathManagementComponent implements OnInit, OnDestroy { }), ) .subscribe({ + next: (res) => { + this.course = res.body!; + }, error: (res: HttpErrorResponse) => onError(this.alertService, res), }); } From 50d5cc23f9615a9c25ab13eeb56d91b4b09da46a Mon Sep 17 00:00:00 2001 From: MaximilianAnzinger Date: Mon, 3 Jul 2023 09:22:27 +0200 Subject: [PATCH 038/215] fix endpoint --- .../www1/artemis/repository/CourseRepository.java | 8 ++++++++ .../in/www1/artemis/web/rest/CourseResource.java | 15 +++++++++++++++ .../learning-path-management.component.ts | 2 +- .../course/manage/course-management.service.ts | 10 ++++++++++ 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java index 3e3fe087d540..7464acbe3777 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java @@ -154,6 +154,9 @@ SELECT CASE WHEN (count(c) > 0) THEN true ELSE false END @EntityGraph(type = LOAD, attributePaths = { "competencies", "prerequisites" }) Optional findWithEagerCompetenciesById(long courseId); + @EntityGraph(type = LOAD, attributePaths = { "learningPaths" }) + Optional findWithEagerLearningPathsById(long courseId); + @EntityGraph(type = LOAD, attributePaths = { "competencies", "learningPaths", "learningPaths.competencies" }) Optional findWithEagerLearningPathsAndCompetenciesById(long courseId); @@ -434,6 +437,11 @@ default Course findWithEagerCompetenciesByIdElseThrow(long courseId) { return findWithEagerCompetenciesById(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId)); } + @NotNull + default Course findWithEagerLearningPathsByIdElseThrow(long courseId) { + return findWithEagerLearningPathsById(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId)); + } + @NotNull default Course findWithEagerLearningPathsAndCompetenciesByIdElseThrow(long courseId) { return findWithEagerLearningPathsAndCompetenciesById(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId)); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java index 6a87454ca3b7..9d7e0048246d 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java @@ -629,6 +629,21 @@ public ResponseEntity getCourseWithOrganizations(@PathVariable Long cour return ResponseEntity.ok(course); } + /** + * GET /courses/:courseId/with-learning-paths Get a course by id with eagerly loaded learning paths + * + * @param courseId the id of the course + * @return the course with eagerly loaded learning paths + */ + @GetMapping("courses/{courseId}/with-learning-paths") + @EnforceAtLeastInstructor + public ResponseEntity getCourseWithLearningPaths(@PathVariable Long courseId) { + log.debug("REST request to get a course with its organizations : {}", courseId); + Course course = courseRepository.findWithEagerLearningPathsByIdElseThrow(courseId); + authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null); + return ResponseEntity.ok(course); + } + /** * GET /courses/:courseId/lockedSubmissions Get locked submissions for course for user * diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts index 205ec27ba25c..4cb66827b72c 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts @@ -122,7 +122,7 @@ export class LearningPathManagementComponent implements OnInit, OnDestroy { private loadData() { this.isLoading = true; - this.courseSub = this.courseManagementService.find(this.courseId).subscribe((courseResponse) => { + this.courseSub = this.courseManagementService.findWithLearningPaths(this.courseId).subscribe((courseResponse) => { this.course = courseResponse.body!; }); diff --git a/src/main/webapp/app/course/manage/course-management.service.ts b/src/main/webapp/app/course/manage/course-management.service.ts index 68cd769eaca9..15c01e3788ca 100644 --- a/src/main/webapp/app/course/manage/course-management.service.ts +++ b/src/main/webapp/app/course/manage/course-management.service.ts @@ -134,6 +134,16 @@ export class CourseManagementService { .pipe(map((res: EntityResponseType) => this.processCourseEntityResponseType(res))); } + /** + * finds a course with the given id and eagerly loaded learning pahts + * @param courseId the id of the course to be found + */ + findWithLearningPaths(courseId: number): Observable { + return this.http + .get(`${this.resourceUrl}/${courseId}/with-learning-paths`, { observe: 'response' }) + .pipe(map((res: EntityResponseType) => this.processCourseEntityResponseType(res))); + } + // TODO: separate course overview and course management REST API calls in a better way /** * finds all courses using a GET request From a1b99437cb90b3037112f4fabf4eea1f136b1d1e Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Mon, 3 Jul 2023 10:19:39 +0200 Subject: [PATCH 039/215] add LearningPathUtilService --- .../competency/LearningPathUtilService.java | 26 +++++++++++++++++++ .../lecture/LearningPathIntegrationTest.java | 25 ++++++++---------- 2 files changed, 37 insertions(+), 14 deletions(-) create mode 100644 src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java diff --git a/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java b/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java new file mode 100644 index 000000000000..7d5d3e7ebbdd --- /dev/null +++ b/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java @@ -0,0 +1,26 @@ +package de.tum.in.www1.artemis.competency; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.repository.CourseRepository; +import de.tum.in.www1.artemis.service.LearningPathService; + +@Service +public class LearningPathUtilService { + + @Autowired + CourseRepository courseRepository; + + @Autowired + LearningPathService learningPathService; + + public Course enableAndGenerateLearningPathsForCourse(Course course) { + course = courseRepository.findWithEagerLearningPathsAndCompetenciesByIdElseThrow(course.getId()); + learningPathService.generateLearningPaths(course); + course.setLeanringPathsEnabled(true); + return courseRepository.save(course); + } + +} diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java index de8b35ff1679..d9325a99e640 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java @@ -17,6 +17,7 @@ import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; import de.tum.in.www1.artemis.competency.CompetencyUtilService; +import de.tum.in.www1.artemis.competency.LearningPathUtilService; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.competency.Competency; @@ -84,6 +85,9 @@ class LearningPathIntegrationTest extends AbstractSpringIntegrationBambooBitbuck @Autowired CompetencyProgressService competencyProgressService; + @Autowired + LearningPathUtilService learningPathUtilService; + private Course course; private Competency[] competencies; @@ -132,13 +136,6 @@ void setupTestScenario() throws Exception { lectureUnitService.setLectureUnitCompletion(textUnit, student, true); } - private void enableLearningPathsForTestingCourse() { - course = courseRepository.findWithEagerLearningPathsAndCompetenciesByIdElseThrow(course.getId()); - learningPathService.generateLearningPaths(course); - course.setLeanringPathsEnabled(true); - course = courseRepository.save(course); - } - private ZonedDateTime now() { return ZonedDateTime.now(); } @@ -225,7 +222,7 @@ void testGenerateLearningPathOnEnrollment() throws Exception { course.setEnrollmentStartDate(past(1)); course.setEnrollmentEndDate(future(1)); course = courseRepository.save(course); - enableLearningPathsForTestingCourse(); + course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); this.setupEnrollmentRequestMocks(); @@ -253,7 +250,7 @@ void testGetLearningPathsOnPageForCourseLearningPathsDisabled() throws Exception @Test @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") void testGetLearningPathsOnPageForCourseEmpty() throws Exception { - enableLearningPathsForTestingCourse(); + course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); final var search = pageableSearchUtilService.configureSearch(STUDENT_OF_COURSE + "SuffixThatAllowsTheResultToBeEmpty"); final var result = request.getSearchResult("/api/courses/" + course.getId() + "/learning-paths", HttpStatus.OK, LearningPath.class, pageableSearchUtilService.searchMapping(search)); @@ -263,7 +260,7 @@ void testGetLearningPathsOnPageForCourseEmpty() throws Exception { @Test @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") void testGetLearningPathsOnPageForCourseExactlyStudent() throws Exception { - enableLearningPathsForTestingCourse(); + course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); final var search = pageableSearchUtilService.configureSearch(STUDENT_OF_COURSE); final var result = request.getSearchResult("/api/courses/" + course.getId() + "/learning-paths", HttpStatus.OK, LearningPath.class, pageableSearchUtilService.searchMapping(search)); @@ -273,7 +270,7 @@ void testGetLearningPathsOnPageForCourseExactlyStudent() throws Exception { @Test @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") void testAddCompetencyToLearningPathsOnCreateCompetency() throws Exception { - enableLearningPathsForTestingCourse(); + course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); final var createdCompetency = createCompetencyRESTCall(); @@ -289,7 +286,7 @@ void testAddCompetencyToLearningPathsOnCreateCompetency() throws Exception { @Test @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") void testAddCompetencyToLearningPathsOnImportCompetency() throws Exception { - enableLearningPathsForTestingCourse(); + course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); final var importedCompetency = importCompetencyRESTCall(); @@ -305,7 +302,7 @@ void testAddCompetencyToLearningPathsOnImportCompetency() throws Exception { @Test @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") void testRemoveCompetencyFromLearningPathsOnDeleteCompetency() throws Exception { - enableLearningPathsForTestingCourse(); + course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); deleteCompetencyRESTCall(competencies[0]); @@ -321,7 +318,7 @@ void testRemoveCompetencyFromLearningPathsOnDeleteCompetency() throws Exception @Test @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") void testUpdateLearningPathProgress() throws Exception { - enableLearningPathsForTestingCourse(); + course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); // add competency with completed learning unit final var createdCompetency = createCompetencyRESTCall(); From 757ad9c9db122aa40651291654443c587e5457a0 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Mon, 3 Jul 2023 10:53:36 +0200 Subject: [PATCH 040/215] improve documentation --- .../competency/CompetencyUtilService.java | 20 +++++++++++++++++++ .../competency/LearningPathUtilService.java | 9 +++++++++ 2 files changed, 29 insertions(+) diff --git a/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java b/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java index c78015566a40..fbe30bd68071 100644 --- a/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java @@ -16,6 +16,13 @@ public class CompetencyUtilService { @Autowired private CompetencyRepository competencyRepo; + /** + * Creates competency and links it to the course. The title of the competency will hold the specified suffix. + * + * @param course course the competency will be linked to + * @param suffix the suffix that will be included in the title + * @return the persisted competency + */ private Competency createCompetency(Course course, String suffix) { Competency competency = new Competency(); competency.setTitle("Example Competency" + suffix); @@ -24,10 +31,23 @@ private Competency createCompetency(Course course, String suffix) { return competencyRepo.save(competency); } + /** + * Creates competency and links it to the course. + * + * @param course course the competency will be linked to + * @return the persisted competency + */ public Competency createCompetency(Course course) { return createCompetency(course, ""); } + /** + * Creates multiple competencies and links them to the course + * + * @param course course the competencies will be linked to + * @param numberOfCompetencies number of competencies to create + * @return array of the persisted competencies + */ public Competency[] createCompetencies(Course course, int numberOfCompetencies) { Competency[] competencies = new Competency[numberOfCompetencies]; for (int i = 0; i < competencies.length; i++) { diff --git a/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java b/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java index 7d5d3e7ebbdd..e05ba4c31dfb 100644 --- a/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java @@ -7,6 +7,9 @@ import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.service.LearningPathService; +/** + * Service responsible for initializing the database with specific testdata related to learning paths for use in integration tests. + */ @Service public class LearningPathUtilService { @@ -16,6 +19,12 @@ public class LearningPathUtilService { @Autowired LearningPathService learningPathService; + /** + * Enable and generate learning paths for course. + * + * @param course the course for which the learning paths are generated + * @return the updated course + */ public Course enableAndGenerateLearningPathsForCourse(Course course) { course = courseRepository.findWithEagerLearningPathsAndCompetenciesByIdElseThrow(course.getId()); learningPathService.generateLearningPaths(course); From 6dc60d54e47798bce1f330949c2a66d868dd9321 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Mon, 3 Jul 2023 11:06:09 +0200 Subject: [PATCH 041/215] Fix compiler error --- .../learning-path-paging.service.spec.ts | 19 ++++++++++++++----- .../service/learning-path.service.spec.ts | 18 ++++++++++++++---- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/test/javascript/spec/service/learning-path-paging.service.spec.ts b/src/test/javascript/spec/service/learning-path-paging.service.spec.ts index 15acf56fc044..d8fdbe59a6e0 100644 --- a/src/test/javascript/spec/service/learning-path-paging.service.spec.ts +++ b/src/test/javascript/spec/service/learning-path-paging.service.spec.ts @@ -1,18 +1,27 @@ import { MockHttpService } from '../helpers/mocks/service/mock-http.service'; import { LearningPathPagingService } from 'app/course/learning-paths/learning-path-paging.service'; import { PageableSearch, SortingOrder } from 'app/shared/table/pageable-table'; -import { HttpParams } from '@angular/common/http'; +import { HttpClient, HttpParams } from '@angular/common/http'; import { TableColumn } from 'app/course/learning-paths/learning-path-management/learning-path-management.component'; +import { ArtemisTestModule } from '../test.module'; +import { TestBed } from '@angular/core/testing'; describe('LearningPathPagingService', () => { let learningPathPagingService: LearningPathPagingService; - let httpService: MockHttpService; + let httpService: HttpClient; let getStub: jest.SpyInstance; beforeEach(() => { - httpService = new MockHttpService(); - learningPathPagingService = new LearningPathPagingService(httpService); - getStub = jest.spyOn(httpService, 'get'); + TestBed.configureTestingModule({ + imports: [ArtemisTestModule], + providers: [{ provide: HttpClient, useClass: MockHttpService }], + }) + .compileComponents() + .then(() => { + httpService = TestBed.inject(HttpClient); + learningPathPagingService = new LearningPathPagingService(httpService); + getStub = jest.spyOn(httpService, 'get'); + }); }); afterEach(() => { diff --git a/src/test/javascript/spec/service/learning-path.service.spec.ts b/src/test/javascript/spec/service/learning-path.service.spec.ts index 1d46f57ce2ae..0f7241ac4099 100644 --- a/src/test/javascript/spec/service/learning-path.service.spec.ts +++ b/src/test/javascript/spec/service/learning-path.service.spec.ts @@ -1,15 +1,25 @@ import { MockHttpService } from '../helpers/mocks/service/mock-http.service'; import { LearningPathService } from 'app/course/learning-paths/learning-path.service'; +import { ArtemisTestModule } from '../test.module'; +import { HttpClient } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; describe('LearningPathService', () => { let learningPathService: LearningPathService; - let httpService: MockHttpService; + let httpService: HttpClient; let putStub: jest.SpyInstance; beforeEach(() => { - httpService = new MockHttpService(); - learningPathService = new LearningPathService(httpService); - putStub = jest.spyOn(httpService, 'put'); + TestBed.configureTestingModule({ + imports: [ArtemisTestModule], + providers: [{ provide: HttpClient, useClass: MockHttpService }], + }) + .compileComponents() + .then(() => { + httpService = TestBed.inject(HttpClient); + learningPathService = new LearningPathService(httpService); + putStub = jest.spyOn(httpService, 'put'); + }); }); afterEach(() => { From 59e37850eedc099c111c2941f36608fcfeb0717a Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Mon, 3 Jul 2023 11:53:43 +0200 Subject: [PATCH 042/215] Fix update for LPs without competencies --- .../artemis/service/LearningPathService.java | 18 ++++++++++++++++-- .../lecture/LearningPathIntegrationTest.java | 18 +++++++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index e144c9dea3e1..b6f9e800d25d 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -65,13 +65,15 @@ public void generateLearningPathForUser(@NotNull Course course, @NotNull User us lpToCreate.getCompetencies().addAll(course.getCompetencies()); var persistedLearningPath = learningPathRepository.save(lpToCreate); log.debug("Created LearningPath (id={}) for user (id={}) in course (id={})", persistedLearningPath.getId(), user.getId(), course.getId()); + updateLearningPathProgress(persistedLearningPath); } } /** * Search for all learning paths fitting a {@link PageableSearchDTO search query}. The result is paged. * - * @param search The search query defining the search term and the size of the returned page + * @param search the search query defining the search term and the size of the returned page + * @param course the course the learning paths are linked to * @return A wrapper object containing a list of all found learning paths and the total number of pages */ public SearchResultPageDTO getAllOfCourseOnPageWithSize(final PageableSearchDTO search, final Course course) { @@ -114,11 +116,22 @@ public void updateLearningPathProgress(final long learningPathId) { this.updateLearningPathProgress(learningPath); } + /** + * Updates progress of the learning path specified by course and user id. + * + * @param courseId id of the course the learning path is linked to + * @param userId id of the user the learning path is linked to + */ public void updateLearningPathProgress(final long courseId, final long userId) { final var learningPath = learningPathRepository.findWithEagerCompetenciesByCourseIdAndUserId(courseId, userId); learningPath.ifPresent(this::updateLearningPathProgress); } + /** + * Updates progress of the given learning path. Competencies of the learning path must be loaded eagerly. + * + * @param learningPath learning path that is updated + */ private void updateLearningPathProgress(final LearningPath learningPath) { final var userId = learningPath.getUser().getId(); final var competencyIds = learningPath.getCompetencies().stream().map(Competency::getId).collect(Collectors.toSet()); @@ -126,7 +139,8 @@ private void updateLearningPathProgress(final LearningPath learningPath) { // TODO: consider optional competencies final var completed = (float) competencyProgresses.stream().filter(CompetencyProgressService::isMastered).count(); - learningPath.setProgress(Math.round(completed * 100 / (float) learningPath.getCompetencies().size())); + final var numberOfCompetencies = learningPath.getCompetencies().size(); + learningPath.setProgress(numberOfCompetencies == 0 ? 0 : Math.round(completed * 100 / (float) numberOfCompetencies)); learningPathRepository.save(learningPath); log.debug("Updated LearningPath (id={}) for user (id={})", learningPath.getId(), userId); } diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java index d9325a99e640..675f5261d494 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java @@ -154,6 +154,10 @@ private void testAllPreAuthorize() throws Exception { request.getSearchResult("/api/courses/" + course.getId() + "/learning-paths", HttpStatus.FORBIDDEN, LearningPath.class, pageableSearchUtilService.searchMapping(search)); } + private Course enableLearningPathsRESTCall(Course course) throws Exception { + return request.putWithResponseBody("/api/courses/" + course.getId() + "/learning-paths/enable", course, Course.class, HttpStatus.OK); + } + private Competency createCompetencyRESTCall() throws Exception { final var competencyToCreate = new Competency(); competencyToCreate.setTitle("CompetencyToCreateTitle"); @@ -198,7 +202,7 @@ void testAll_asEditor() throws Exception { @Test @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") void testEnableLearningPaths() throws Exception { - request.putWithResponseBody("/api/courses/" + course.getId() + "/learning-paths/enable", course, Course.class, HttpStatus.OK); + enableLearningPathsRESTCall(course); final var updatedCourse = courseRepository.findWithEagerLearningPathsAndCompetenciesByIdElseThrow(course.getId()); assertThat(updatedCourse.getLearningPathsEnabled()).as("should enable LearningPaths").isTrue(); assertThat(updatedCourse.getLearningPaths()).isNotNull(); @@ -207,6 +211,18 @@ void testEnableLearningPaths() throws Exception { lp -> assertThat(lp.getCompetencies().size()).as("LearningPath (id={}) should have be linked to all Competencies", lp.getId()).isEqualTo(competencies.length)); } + @Test + @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") + void testEnableLearningPathsWithNoCompetencies() throws Exception { + var courseWithoutCompetencies = courseUtilService.createCoursesWithExercisesAndLectures(TEST_PREFIX, false, false, 0).get(0); + enableLearningPathsRESTCall(courseWithoutCompetencies); + final var updatedCourse = courseRepository.findWithEagerLearningPathsAndCompetenciesByIdElseThrow(courseWithoutCompetencies.getId()); + assertThat(updatedCourse.getLearningPathsEnabled()).as("should enable LearningPaths").isTrue(); + assertThat(updatedCourse.getLearningPaths()).isNotNull(); + assertThat(updatedCourse.getLearningPaths().size()).as("should create LearningPath for each student").isEqualTo(NUMBER_OF_STUDENTS); + updatedCourse.getLearningPaths().forEach(lp -> assertThat(lp.getProgress()).as("LearningPath (id={}) should have no progress", lp.getId()).isEqualTo(0)); + } + @Test @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") void testEnableLearningPathsAlreadyEnabled() throws Exception { From 618892f787427e68f822c34d89885ac538255854 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Tue, 4 Jul 2023 15:44:52 +0200 Subject: [PATCH 043/215] Improve search and test coverage --- .../domain/competency/LearningPath.java | 4 +-- .../artemis/repository/CourseRepository.java | 10 +++++-- .../repository/LearningPathRepository.java | 4 +-- .../artemis/service/LearningPathService.java | 2 +- .../learning-path-management.component.html | 4 +-- .../learning-path-management.component.ts | 19 +++++++------- src/main/webapp/app/entities/course.model.ts | 4 ++- .../artemis/course/CourseTestService.java | 26 +++++++++++++++++++ ...rseBitbucketBambooJiraIntegrationTest.java | 6 +++++ .../CourseGitlabJenkinsIntegrationTest.java | 6 +++++ 10 files changed, 65 insertions(+), 20 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java b/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java index f6a0bb270386..c3897b5e7f46 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java @@ -8,7 +8,6 @@ import org.hibernate.annotations.Cache; import org.hibernate.annotations.CacheConcurrencyStrategy; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import de.tum.in.www1.artemis.domain.Course; @@ -25,7 +24,6 @@ public class LearningPath extends DomainObject { @ManyToOne @JoinColumn(name = "user_id") - @JsonIgnore private User user; @ManyToOne @@ -85,7 +83,7 @@ public String toString() { public enum LearningPathSearchColumn { - ID("id"), STUDENT_LOGIN("student.login"); + ID("id"), USER_LOGIN("user.login"), USER_NAME("user.lastName"), PROGRESS("progress"); private final String mappedColumnName; diff --git a/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java index baed6adf8030..ec762da4231f 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java @@ -141,8 +141,14 @@ SELECT CASE WHEN (count(c) > 0) THEN true ELSE false END @EntityGraph(type = LOAD, attributePaths = { "competencies", "prerequisites" }) Optional findWithEagerCompetenciesById(long courseId); - @EntityGraph(type = LOAD, attributePaths = { "learningPaths" }) - Optional findWithEagerLearningPathsById(long courseId); + @Query(""" + SELECT c + FROM Course c + LEFT JOIN FETCH c.learningPaths lp + LEFT JOIN FETCH lp.user + WHERE c.id = :courseId + """) + Optional findWithEagerLearningPathsById(@Param("courseId") long courseId); @EntityGraph(type = LOAD, attributePaths = { "competencies", "learningPaths", "learningPaths.competencies" }) Optional findWithEagerLearningPathsAndCompetenciesById(long courseId); diff --git a/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java index 262a40536eff..f029baec0790 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java @@ -40,9 +40,9 @@ default LearningPath findWithEagerCompetenciesByCourseIdAndUserIdElseThrow(long @Query(""" SELECT lp FROM LearningPath lp - WHERE (lp.course.id = :courseId) AND (lp.user.login LIKE %:partialLogin%) + WHERE (lp.course.id = :courseId) AND (lp.user.login LIKE %:searchTerm% OR lp.user.firstName LIKE %:searchTerm% OR lp.user.lastName LIKE %:searchTerm%) """) - Page findByLoginInCourse(@Param("partialLogin") String partialLogin, @Param("courseId") long courseId, Pageable pageable); + Page findByLoginOrNameInCourse(@Param("searchTerm") String searchTerm, @Param("courseId") long courseId, Pageable pageable); @EntityGraph(type = LOAD, attributePaths = { "competencies" }) Optional findWithEagerCompetenciesById(long learningPathId); diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index b6f9e800d25d..fe1b81e6815c 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -79,7 +79,7 @@ public void generateLearningPathForUser(@NotNull Course course, @NotNull User us public SearchResultPageDTO getAllOfCourseOnPageWithSize(final PageableSearchDTO search, final Course course) { final var pageable = PageUtil.createLearningPathPageRequest(search); final var searchTerm = search.getSearchTerm(); - final Page learningPathPage = learningPathRepository.findByLoginInCourse(searchTerm, course.getId(), pageable); + final Page learningPathPage = learningPathRepository.findByLoginOrNameInCourse(searchTerm, course.getId(), pageable); return new SearchResultPageDTO<>(learningPathPage.getContent(), learningPathPage.getTotalPages()); } diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html index b53219e7bc09..f4e59a47562c 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html @@ -6,7 +6,7 @@

Learning Path Management

-
+
Disabled
-
+
Search for Learning Path: diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts index 4cb66827b72c..b83536e0c758 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts @@ -39,7 +39,7 @@ export class LearningPathManagementComponent implements OnInit, OnDestroy { page: 1, pageSize: 50, searchTerm: '', - sortingOrder: SortingOrder.ASCENDING, + sortingOrder: SortingOrder.DESCENDING, sortedColumn: TableColumn.ID, }; content: SearchResult; @@ -111,6 +111,8 @@ export class LearningPathManagementComponent implements OnInit, OnDestroy { } ngOnInit(): void { + this.content = { resultsOnPage: [], numberOfPages: 0 }; + this.activatedRoute.parent!.params.subscribe((params) => { this.courseId = +params['courseId']; if (this.courseId) { @@ -124,16 +126,15 @@ export class LearningPathManagementComponent implements OnInit, OnDestroy { this.courseSub = this.courseManagementService.findWithLearningPaths(this.courseId).subscribe((courseResponse) => { this.course = courseResponse.body!; - }); - - if (this.course?.learningPathEnabled) { - this.content = { resultsOnPage: [], numberOfPages: 0 }; + console.log(this.course); - this.performSearch(this.sort, 0); - this.performSearch(this.search, 300); - } + if (this.course.learningPathsEnabled) { + this.performSearch(this.sort, 0); + this.performSearch(this.search, 300); + } - this.isLoading = false; + this.isLoading = false; + }); } /** diff --git a/src/main/webapp/app/entities/course.model.ts b/src/main/webapp/app/entities/course.model.ts index 9efb8b184486..9b7b28729f8a 100644 --- a/src/main/webapp/app/entities/course.model.ts +++ b/src/main/webapp/app/entities/course.model.ts @@ -11,6 +11,7 @@ import { ProgrammingLanguage } from 'app/entities/programming-exercise.model'; import { OnlineCourseConfiguration } from 'app/entities/online-course-configuration.model'; import { TutorialGroup } from 'app/entities/tutorial-group/tutorial-group.model'; import { TutorialGroupsConfiguration } from 'app/entities/tutorial-group/tutorial-groups-configuration.model'; +import { LearningPath } from 'app/entities/learning-path.model'; export enum CourseInformationSharingConfiguration { COMMUNICATION_AND_MESSAGING = 'COMMUNICATION_AND_MESSAGING', @@ -89,7 +90,8 @@ export class Course implements BaseEntity { public lectures?: Lecture[]; public competencies?: Competency[]; public prerequisites?: Competency[]; - public learningPathEnabled?: boolean; + public learningPathsEnabled?: boolean; + public learningPaths?: LearningPath[]; public exams?: Exam[]; public organizations?: Organization[]; public tutorialGroups?: TutorialGroup[]; diff --git a/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java b/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java index 0e348794c4ab..0a57cd54f2f0 100644 --- a/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java +++ b/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java @@ -43,6 +43,7 @@ import de.tum.in.www1.artemis.assessment.ComplaintUtilService; import de.tum.in.www1.artemis.competency.CompetencyUtilService; +import de.tum.in.www1.artemis.competency.LearningPathUtilService; import de.tum.in.www1.artemis.config.Constants; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.competency.Competency; @@ -212,6 +213,9 @@ public class CourseTestService { @Autowired private TeamUtilService teamUtilService; + @Autowired + private LearningPathUtilService learningPathUtilService; + private static final int numberOfStudents = 8; private static final int numberOfTutors = 5; @@ -3134,4 +3138,26 @@ public void testEditCourseRemoveExistingIcon() throws Exception { private String getUpdateOnlineCourseConfigurationPath(String courseId) { return "/api/courses/" + courseId + "/onlineCourseConfiguration"; } + + // Test + public void testFindWithLearningPaths_AsInstructor() throws Exception { + String testSuffix = "findwithlearningpaths"; + adjustUserGroupsToCustomGroups(testSuffix); + + var course = courseUtilService.createCourse(); + adjustCourseGroups(course, testSuffix); + course = courseRepo.save(course); + course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); + + final var result = request.get("/api/courses/" + course.getId() + "/with-learning-paths", HttpStatus.OK, Course.class); + + assertThat(result.getLearningPathsEnabled()).isTrue(); + assertThat(result.getLearningPaths().size()).as("all learning paths are returned").isEqualTo(8); + final var anyLearningPath = result.getLearningPaths().stream().findAny().orElseThrow(); + assertThat(anyLearningPath.getUser()).isNotNull(); + final var student = anyLearningPath.getUser(); + assertThat(student.getLogin()).as("associated student has login").isNotEmpty(); + assertThat(student.getFirstName()).as("associated student has first name").isNotEmpty(); + assertThat(student.getLastName()).as("associated student has last name").isNotEmpty(); + } } diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseBitbucketBambooJiraIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseBitbucketBambooJiraIntegrationTest.java index cf94dabc272d..868a479b00fa 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseBitbucketBambooJiraIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseBitbucketBambooJiraIntegrationTest.java @@ -918,4 +918,10 @@ void testUpdateValidOnlineCourseConfiguration() throws Exception { void testEditCourseRemoveExistingIcon() throws Exception { courseTestService.testEditCourseRemoveExistingIcon(); } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testFindWithLearningPaths_AsInstructor() throws Exception { + courseTestService.testFindWithLearningPaths_AsInstructor(); + } } diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseGitlabJenkinsIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseGitlabJenkinsIntegrationTest.java index f859fca4b856..0d2f26c98d12 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseGitlabJenkinsIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseGitlabJenkinsIntegrationTest.java @@ -1036,4 +1036,10 @@ void testUpdateValidOnlineCourseConfiguration() throws Exception { void testEditCourseRemoveExistingIcon() throws Exception { courseTestService.testEditCourseRemoveExistingIcon(); } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testFindWithLearningPaths_AsInstructor() throws Exception { + courseTestService.testFindWithLearningPaths_AsInstructor(); + } } From 144505fbb8fd3fb863ea1899878c08c613b0ef54 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Wed, 5 Jul 2023 06:38:51 +0200 Subject: [PATCH 044/215] fix client test --- .../learning-path-management.component.ts | 3 +- .../course/course-management.service.spec.ts | 13 +++++ ...learning-path-management.component.spec.ts | 52 ++++++++++++++----- 3 files changed, 53 insertions(+), 15 deletions(-) diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts index b83536e0c758..b940cabed71b 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts @@ -39,7 +39,7 @@ export class LearningPathManagementComponent implements OnInit, OnDestroy { page: 1, pageSize: 50, searchTerm: '', - sortingOrder: SortingOrder.DESCENDING, + sortingOrder: SortingOrder.ASCENDING, sortedColumn: TableColumn.ID, }; content: SearchResult; @@ -126,7 +126,6 @@ export class LearningPathManagementComponent implements OnInit, OnDestroy { this.courseSub = this.courseManagementService.findWithLearningPaths(this.courseId).subscribe((courseResponse) => { this.course = courseResponse.body!; - console.log(this.course); if (this.course.learningPathsEnabled) { this.performSearch(this.sort, 0); diff --git a/src/test/javascript/spec/component/course/course-management.service.spec.ts b/src/test/javascript/spec/component/course/course-management.service.spec.ts index 41d6d0fa6ec2..6aaf0fa42ad9 100644 --- a/src/test/javascript/spec/component/course/course-management.service.spec.ts +++ b/src/test/javascript/spec/component/course/course-management.service.spec.ts @@ -26,6 +26,7 @@ import { CourseScores } from 'app/course/course-scores/course-scores'; import { ScoresStorageService } from 'app/course/course-scores/scores-storage.service'; import { CourseStorageService } from 'app/course/manage/course-storage.service'; import { Result } from 'app/entities/result.model'; +import { LearningPath } from 'app/entities/learning-path.model'; describe('Course Management Service', () => { let courseManagementService: CourseManagementService; @@ -203,6 +204,18 @@ describe('Course Management Service', () => { tick(); })); + it('should find course with learning paths', fakeAsync(() => { + course.learningPathsEnabled = true; + course.learningPaths = [new LearningPath()]; + returnedFromService = { ...course }; + courseManagementService + .findWithLearningPaths(course.id!) + .pipe(take(1)) + .subscribe((res) => expect(res.body).toEqual(course)); + requestAndExpectDateConversion('GET', `${resourceUrl}/${course.id}/with-learning-paths`, returnedFromService, course); + tick(); + })); + it('should find all courses for dashboard', fakeAsync(() => { const courseStorageServiceSpy = jest.spyOn(courseStorageService, 'setCourses'); returnedFromService = [{ ...courseForDashboard }]; diff --git a/src/test/javascript/spec/component/learning-paths/learning-path-management.component.spec.ts b/src/test/javascript/spec/component/learning-paths/learning-path-management.component.spec.ts index d661106eb0a2..27d290f581c8 100644 --- a/src/test/javascript/spec/component/learning-paths/learning-path-management.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/learning-path-management.component.spec.ts @@ -5,7 +5,7 @@ import { SortService } from 'app/shared/service/sort.service'; import { PageableSearch, SearchResult, SortingOrder } from 'app/shared/table/pageable-table'; import { LearningPath } from 'app/entities/learning-path.model'; import { ArtemisTestModule } from '../../test.module'; -import { MockComponent, MockDirective } from 'ng-mocks'; +import { MockComponent, MockDirective, MockProvider } from 'ng-mocks'; import { ButtonComponent } from 'app/shared/components/button.component'; import { NgbPagination } from '@ng-bootstrap/ng-bootstrap'; import { SortByDirective } from 'app/shared/sort/sort-by.directive'; @@ -13,6 +13,8 @@ import { SortDirective } from 'app/shared/sort/sort.directive'; import { of } from 'rxjs'; import { CourseManagementService } from 'app/course/manage/course-management.service'; import { Course } from 'app/entities/course.model'; +import { ActivatedRoute } from '@angular/router'; +import { HttpResponse } from '@angular/common/http'; describe('LearningPathManagementComponent', () => { let fixture: ComponentFixture; @@ -31,13 +33,26 @@ describe('LearningPathManagementComponent', () => { TestBed.configureTestingModule({ imports: [ArtemisTestModule, MockComponent(NgbPagination)], declarations: [LearningPathManagementComponent, MockComponent(ButtonComponent), MockDirective(SortByDirective), MockDirective(SortDirective)], + providers: [ + { + provide: ActivatedRoute, + useValue: { + parent: { + params: of({ + courseId: 1, + }), + }, + }, + }, + MockProvider(CourseManagementService), + ], }) .compileComponents() .then(() => { fixture = TestBed.createComponent(LearningPathManagementComponent); comp = fixture.componentInstance; courseManagementService = TestBed.inject(CourseManagementService); - findCourseStub = jest.spyOn(courseManagementService, 'find'); + findCourseStub = jest.spyOn(courseManagementService, 'findWithLearningPaths'); pagingService = TestBed.inject(LearningPathPagingService); sortService = TestBed.inject(SortService); searchForLearningPathsStub = jest.spyOn(pagingService, 'searchForLearningPaths'); @@ -51,11 +66,13 @@ describe('LearningPathManagementComponent', () => { beforeEach(() => { fixture.detectChanges(); - course = new Course(); - course.learningPathEnabled = true; - findCourseStub.mockReturnValue(of(course)); learningPath = new LearningPath(); learningPath.id = 1; + course = new Course(); + course.id = 1; + course.learningPathsEnabled = true; + course.learningPaths = [learningPath]; + findCourseStub.mockReturnValue(of(new HttpResponse({ body: course }))); searchResult = { numberOfPages: 3, resultsOnPage: [learningPath] }; state = { page: 1, @@ -77,13 +94,22 @@ describe('LearningPathManagementComponent', () => { expect(sortByPropertyStub).toHaveBeenCalledWith(searchResult.resultsOnPage, comp.sortedColumn, comp.listSorting); }; - it('should set content to paging result on sort', fakeAsync(() => { - expect(comp.listSorting).toBeFalse(); + it('should load course on init', fakeAsync(() => { setStateAndCallOnInit(() => { comp.listSorting = true; tick(10); - expect(searchForLearningPathsStub).toHaveBeenCalledWith({ ...state, sortingOrder: SortingOrder.ASCENDING }); - expect(comp.listSorting).toBeTrue(); + expect(findCourseStub).toHaveBeenCalledWith(1); + expect(comp.course).toEqual(course); + }); + })); + + it('should set content to paging result on sort', fakeAsync(() => { + expect(comp.listSorting).toBeTrue(); + setStateAndCallOnInit(() => { + comp.listSorting = false; + tick(10); + expect(searchForLearningPathsStub).toHaveBeenCalledWith({ ...state, sortingOrder: SortingOrder.DESCENDING }, 1); + expect(comp.listSorting).toBeFalse(); }); })); @@ -92,7 +118,7 @@ describe('LearningPathManagementComponent', () => { setStateAndCallOnInit(() => { comp.onPageChange(5); tick(10); - expect(searchForLearningPathsStub).toHaveBeenCalledWith({ ...state, page: 5 }); + expect(searchForLearningPathsStub).toHaveBeenCalledWith({ ...state, page: 5 }, course.id); expect(comp.page).toBe(5); }); })); @@ -105,7 +131,7 @@ describe('LearningPathManagementComponent', () => { tick(10); expect(searchForLearningPathsStub).not.toHaveBeenCalled(); tick(290); - expect(searchForLearningPathsStub).toHaveBeenCalledWith({ ...state, searchTerm: givenSearchTerm }); + expect(searchForLearningPathsStub).toHaveBeenCalledWith({ ...state, searchTerm: givenSearchTerm }, course.id); expect(comp.searchTerm).toEqual(givenSearchTerm); }); })); @@ -115,12 +141,12 @@ describe('LearningPathManagementComponent', () => { setStateAndCallOnInit(() => { comp.sortedColumn = TableColumn.USER_LOGIN; tick(10); - expect(searchForLearningPathsStub).toHaveBeenCalledWith({ ...state, sortedColumn: TableColumn.USER_LOGIN }); + expect(searchForLearningPathsStub).toHaveBeenCalledWith({ ...state, sortedColumn: TableColumn.USER_LOGIN }, course.id); expect(comp.sortedColumn).toEqual(TableColumn.USER_LOGIN); }); })); - it('should return competency id', () => { + it('should return learning path id', () => { expect(comp.trackId(0, learningPath)).toEqual(learningPath.id); }); }); From 23c355dca95acc18fcbe977b7d82591d7755bf26 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Wed, 5 Jul 2023 14:46:12 +0200 Subject: [PATCH 045/215] add generation of ngx representation --- build.gradle | 1 + .../artemis/service/LearningPathService.java | 168 +++++++++++++++++- .../dto/learningpath/NgxLearningPathDTO.java | 84 +++++++++ 3 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/NgxLearningPathDTO.java diff --git a/build.gradle b/build.gradle index 5fdf6ebff5e9..f397aff98791 100644 --- a/build.gradle +++ b/build.gradle @@ -340,6 +340,7 @@ dependencies { implementation "de.tum.in.ase.athene:client:0.0.2" implementation "commons-fileupload:commons-fileupload:1.5" implementation "net.lingala.zip4j:zip4j:2.11.5" + implementation "org.jgrapht:jgrapht-core:1.5.2" annotationProcessor "org.hibernate:hibernate-jpamodelgen:${hibernate_version}" diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index fe1b81e6815c..c9d3a2c95c16 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -1,9 +1,14 @@ package de.tum.in.www1.artemis.service; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; +import java.util.stream.LongStream; +import java.util.stream.Stream; import javax.validation.constraints.NotNull; +import org.jgrapht.alg.util.UnionFind; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; @@ -12,10 +17,12 @@ import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.competency.Competency; +import de.tum.in.www1.artemis.domain.competency.CompetencyRelation; import de.tum.in.www1.artemis.domain.competency.LearningPath; import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.web.rest.dto.PageableSearchDTO; import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; +import de.tum.in.www1.artemis.web.rest.dto.learningpath.NgxLearningPathDTO; import de.tum.in.www1.artemis.web.rest.util.PageUtil; @Service @@ -31,12 +38,15 @@ public class LearningPathService { private final CourseRepository courseRepository; + private final CompetencyRelationRepository competencyRelationRepository; + public LearningPathService(UserRepository userRepository, LearningPathRepository learningPathRepository, CompetencyProgressRepository competencyProgressRepository, - CourseRepository courseRepository) { + CourseRepository courseRepository, CompetencyRelationRepository competencyRelationRepository) { this.userRepository = userRepository; this.learningPathRepository = learningPathRepository; this.competencyProgressRepository = competencyProgressRepository; this.courseRepository = courseRepository; + this.competencyRelationRepository = competencyRelationRepository; } /** @@ -144,4 +154,160 @@ private void updateLearningPathProgress(final LearningPath learningPath) { learningPathRepository.save(learningPath); log.debug("Updated LearningPath (id={}) for user (id={})", learningPath.getId(), userId); } + + /** + * Generates Ngx representation of the learning path. + * + * @param learningPath the learning path for which the Ngx representation should be created + * @return Ngx representation of the learning path + * @see NgxLearningPathDTO + */ + public NgxLearningPathDTO generateNgxRepresentation(LearningPath learningPath) { + Set nodes = new HashSet<>(); + Set edges = new HashSet<>(); + Set clusters = new HashSet<>(); + learningPath.getCompetencies().forEach(competency -> generateNgxRepresentationForCompetency(competency, nodes, edges, clusters)); + generateNgxRepresentationForRelations(learningPath, nodes, edges); + return new NgxLearningPathDTO(nodes, edges, clusters); + } + + /** + * Generates Ngx representation for competency. + *

+ * A competency's representation consists of + *

    + *
  • start node
  • + *
  • end node
  • + *
  • a node for each learning unit (exercises or lecture unit)
  • + *
  • edges from start node to each learning unit
  • + *
  • edges from each learning unit to end node
  • + *
  • a cluster consisting of all created nodes
  • + *
+ * + * @param competency the competency for which the representation will be created + * @param nodes set of nodes to store the new nodes + * @param edges set of edges to store the new edges + * @param clusters set of clusters to store the new clusters + */ + private void generateNgxRepresentationForCompetency(Competency competency, Set nodes, Set edges, + Set clusters) { + Set currentCluster = new HashSet<>(); + // generates start and end node + final var startNodeId = competency.getId() + "-start"; + final var endNodeId = competency.getId() + "-end"; + currentCluster.add(new NgxLearningPathDTO.Node(startNodeId, NgxLearningPathDTO.NodeType.COMPETENCY_START, competency.getId(), "")); + currentCluster.add(new NgxLearningPathDTO.Node(endNodeId, NgxLearningPathDTO.NodeType.COMPETENCY_END, competency.getId(), "")); + + final var prefix = competency.getId() + "-"; + // generate nodes and edges for lecture units + competency.getLectureUnits().forEach(lectureUnit -> { + currentCluster.add(new NgxLearningPathDTO.Node(prefix + lectureUnit.getId(), NgxLearningPathDTO.NodeType.LECTURE_UNIT, lectureUnit.getId(), lectureUnit.getName())); + edges.add(new NgxLearningPathDTO.Edge(prefix + lectureUnit.getId() + "-in", startNodeId, prefix + lectureUnit.getId())); + edges.add(new NgxLearningPathDTO.Edge(prefix + lectureUnit.getId() + "-out", prefix + lectureUnit.getId(), endNodeId)); + }); + // generate nodes and edges for exercises + competency.getExercises().forEach(exercise -> { + currentCluster.add(new NgxLearningPathDTO.Node(prefix + exercise.getId(), NgxLearningPathDTO.NodeType.EXERCISE, exercise.getId(), exercise.getTitle())); + edges.add(new NgxLearningPathDTO.Edge(prefix + exercise.getId() + "-in", startNodeId, prefix + exercise.getId())); + edges.add(new NgxLearningPathDTO.Edge(prefix + exercise.getId() + "-out", prefix + exercise.getId(), endNodeId)); + }); + // if no linked learning units exist directly link start to end + if (currentCluster.size() == 2) { + edges.add(new NgxLearningPathDTO.Edge(prefix, startNodeId, endNodeId)); + } + // generate cluster for competency + var childNodeIds = currentCluster.stream().map(NgxLearningPathDTO.Node::id).collect(Collectors.toSet()); + childNodeIds.add(startNodeId); + childNodeIds.add(endNodeId); + clusters.add(new NgxLearningPathDTO.Cluster("" + competency.getId(), competency.getTitle(), childNodeIds)); + + nodes.addAll(currentCluster); + } + + /** + * Generates Ngx representations for competency relations. + *

+ * The representation will contain: + *

    + *
  • + * For each matching cluster (transitive closure of competencies that are in a match relation): + *
      + *
    • two nodes (start and end of cluster) will be created
    • + *
    • edges from the start node of the cluster to each start node of the competencies
    • + *
    • edges from each end node of the competency to the end node of the cluster
    • + *
    + *
  • + *
  • + * For each other relation: edge from head competency end node to tail competency start node. If competency is part of a matching cluster, the edge will be linked to the + * corresponding cluster start/end node. + *
  • + *
+ * + * two nodes (start and end of cluster) will be created. + * + * @param learningPath the learning path for which the Ngx representation should be created + * @param nodes set of nodes to store the new nodes + * @param edges set of edges to store the new edges + */ + private void generateNgxRepresentationForRelations(LearningPath learningPath, Set nodes, Set edges) { + final var relations = competencyRelationRepository.findAllByCourseId(learningPath.getCourse().getId()); + + // compute match clusters + Map competencyToMatchCluster = new HashMap<>(); + final var competenciesInMatchRelation = relations.stream().filter(relation -> relation.getType().equals(CompetencyRelation.RelationType.MATCHES)) + .flatMap(relation -> Stream.of(relation.getHeadCompetency().getId(), relation.getTailCompetency().getId())).collect(Collectors.toSet()); + if (!competenciesInMatchRelation.isEmpty()) { + UnionFind matchClusters = new UnionFind(competenciesInMatchRelation); + relations.stream().filter(relation -> relation.getType().equals(CompetencyRelation.RelationType.MATCHES)) + .forEach(relation -> matchClusters.union(relation.getHeadCompetency().getId(), relation.getTailCompetency().getId())); + + AtomicInteger matchClusterId = new AtomicInteger(); + relations.stream().filter(relation -> relation.getType().equals(CompetencyRelation.RelationType.MATCHES)) + .flatMapToLong(relation -> LongStream.of(relation.getHeadCompetency().getId(), relation.getTailCompetency().getId())).distinct().forEach(competencyId -> { + var parentId = matchClusters.find(competencyId); + var clusterId = competencyToMatchCluster.computeIfAbsent((Long) parentId, (key) -> matchClusterId.getAndIncrement()); + competencyToMatchCluster.put(competencyId, clusterId); + }); + + // generate match cluster start and end nodes + for (int i = 0; i < matchClusters.numberOfSets(); i++) { + nodes.add(new NgxLearningPathDTO.Node("matching-" + i + "-start", NgxLearningPathDTO.NodeType.COMPETENCY_START, -1, "")); + nodes.add(new NgxLearningPathDTO.Node("matching-" + i + "-end", NgxLearningPathDTO.NodeType.COMPETENCY_END, -1, "")); + } + + // generate edges between match cluster nodes and corresponding competencies + competencyToMatchCluster.entrySet().stream().forEach(entry -> { + edges.add(new NgxLearningPathDTO.Edge(entry.getKey() + "-in", "matching-" + entry.getValue() + "-start", entry.getKey() + "-start")); + edges.add(new NgxLearningPathDTO.Edge(entry.getKey() + "-out", entry.getKey() + "-end", "matching-" + entry.getValue() + "-end")); + }); + } + + // generate edges for remaining relations + final Set createdRelations = new HashSet<>(); + relations.stream().filter(relation -> !relation.getType().equals(CompetencyRelation.RelationType.MATCHES)) + .forEach(relation -> generateNgxRepresentationForRelation(relation, competencyToMatchCluster, createdRelations, edges)); + } + + /** + * Generates Ngx representations for competency relation. + * + * @param relation the relation for which the Ngx representation should be created + * @param competencyToMatchCluster map from competencies to corresponding cluster + * @param createdRelations set of edge ids that have already been created + * @param edges set of edges to store the new edges + */ + private void generateNgxRepresentationForRelation(CompetencyRelation relation, Map competencyToMatchCluster, Set createdRelations, + Set edges) { + final var sourceId = relation.getHeadCompetency().getId(); + final String sourceNodeId = competencyToMatchCluster.containsKey(sourceId) ? "matching-" + competencyToMatchCluster.get(sourceId) + "-end" : sourceId + "-end"; + final var targetId = relation.getTailCompetency().getId(); + final String targetNodeId = competencyToMatchCluster.containsKey(targetId) ? "matching-" + competencyToMatchCluster.get(targetId) + "-start" : sourceId + "-start"; + final String relationEdgeId = "relation-" + sourceNodeId + "-" + targetNodeId; + // skip if relation has already been created (possible for edges linked to matching cluster start/end nodes) + if (!createdRelations.contains(relationEdgeId)) { + final var edge = new NgxLearningPathDTO.Edge(relationEdgeId, sourceNodeId, targetNodeId); + edges.add(edge); + createdRelations.add(relationEdgeId); + } + } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/NgxLearningPathDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/NgxLearningPathDTO.java new file mode 100644 index 000000000000..4b82c9537083 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/NgxLearningPathDTO.java @@ -0,0 +1,84 @@ +package de.tum.in.www1.artemis.web.rest.dto.learningpath; + +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Represents simplified + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record NgxLearningPathDTO(Set nodes, Set edges, Set clusters) { + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof NgxLearningPathDTO)) { + return false; + } + + final NgxLearningPathDTO other = (NgxLearningPathDTO) obj; + return nodes.equals(other.nodes) && edges.equals(other.edges) && clusters.equals(other.clusters); + } + + @Override + public String toString() { + return "NgxLearningPathDTO{nodes=" + nodes + ", edges=" + edges + ", clusters=" + clusters + "}"; + } + + public record Node(String id, NodeType type, long linkedResource, String label) { + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Node)) { + return false; + } + + final Node other = (Node) obj; + return id.equals(other.id) && type.equals(other.type) && linkedResource == other.linkedResource && label.equals(other.label); + } + + @Override + public String toString() { + return "Node{id=" + id + ", type=" + type.name() + ", linkedResource=" + linkedResource + ", label=" + label + "}"; + } + } + + public record Edge(String id, String source, String target) { + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Edge)) { + return false; + } + + final Edge other = (Edge) obj; + return id.equals(other.id) && source.equals(other.source) && target.equals(other.target); + } + + @Override + public String toString() { + return "Edge{id=" + id + ", source=" + source + ", target=" + target + "}"; + } + } + + public record Cluster(String id, String label, Set childNodeIds) { + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Cluster)) { + return false; + } + final Cluster other = (Cluster) obj; + return id.equals(other.id) && label.equals(other.label) && childNodeIds.equals(other.childNodeIds); + } + + @Override + public String toString() { + return "Cluster{id=" + id + ", label=" + label + ", childNodeIds=" + childNodeIds + "}"; + } + } + + public enum NodeType { + COMPETENCY_START, COMPETENCY_END, MATCH_START, MATCH_END, EXERCISE, LECTURE_UNIT, + } +} From c9797c18e6a0a2e820698509e2d5f1beb58ac13e Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Fri, 7 Jul 2023 16:03:52 +0200 Subject: [PATCH 046/215] Add clustering of matching competencies --- .../repository/LearningPathRepository.java | 8 + .../artemis/service/LearningPathService.java | 103 ++++++-- .../competency/CompetencyUtilService.java | 46 +++- .../competency/LearningPathUtilService.java | 24 ++ .../artemis/lecture/LectureUtilService.java | 1 + .../service/LearningPathServiceTest.java | 247 ++++++++++++++++++ 6 files changed, 407 insertions(+), 22 deletions(-) create mode 100644 src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java diff --git a/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java index f029baec0790..1957bc915992 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java @@ -51,4 +51,12 @@ default LearningPath findWithEagerCompetenciesByCourseIdAndUserIdElseThrow(long default LearningPath findWithEagerCompetenciesByIdElseThrow(long learningPathId) { return findWithEagerCompetenciesById(learningPathId).orElseThrow(() -> new EntityNotFoundException("LearningPath", learningPathId)); } + + @EntityGraph(type = LOAD, attributePaths = { "competencies", "competencies.lectureUnits", "competencies.exercises" }) + Optional findWithEagerCompetenciesAndLearningUnitsById(long learningPathId); + + @NotNull + default LearningPath findWithEagerCompetenciesAndLearningUnitsByIdElseThrow(long learningPathId) { + return findWithEagerCompetenciesAndLearningUnitsById(learningPathId).orElseThrow(() -> new EntityNotFoundException("LearningPath", learningPathId)); + } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index c9d3a2c95c16..6b9e43aa1f42 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -193,33 +193,37 @@ private void generateNgxRepresentationForCompetency(Competency competency, Set clusters) { Set currentCluster = new HashSet<>(); // generates start and end node - final var startNodeId = competency.getId() + "-start"; - final var endNodeId = competency.getId() + "-end"; + final var startNodeId = getCompetencyStartNodeId(competency.getId()); + final var endNodeId = getCompetencyEndNodeId(competency.getId()); currentCluster.add(new NgxLearningPathDTO.Node(startNodeId, NgxLearningPathDTO.NodeType.COMPETENCY_START, competency.getId(), "")); currentCluster.add(new NgxLearningPathDTO.Node(endNodeId, NgxLearningPathDTO.NodeType.COMPETENCY_END, competency.getId(), "")); final var prefix = competency.getId() + "-"; // generate nodes and edges for lecture units competency.getLectureUnits().forEach(lectureUnit -> { - currentCluster.add(new NgxLearningPathDTO.Node(prefix + lectureUnit.getId(), NgxLearningPathDTO.NodeType.LECTURE_UNIT, lectureUnit.getId(), lectureUnit.getName())); - edges.add(new NgxLearningPathDTO.Edge(prefix + lectureUnit.getId() + "-in", startNodeId, prefix + lectureUnit.getId())); - edges.add(new NgxLearningPathDTO.Edge(prefix + lectureUnit.getId() + "-out", prefix + lectureUnit.getId(), endNodeId)); + currentCluster.add(new NgxLearningPathDTO.Node(getLectureUnitNodeId(competency.getId(), lectureUnit.getId()), NgxLearningPathDTO.NodeType.LECTURE_UNIT, + lectureUnit.getId(), lectureUnit.getName())); + edges.add(new NgxLearningPathDTO.Edge(getLectureUnitInEdgeId(competency.getId(), lectureUnit.getId()), startNodeId, + getLectureUnitNodeId(competency.getId(), lectureUnit.getId()))); + edges.add(new NgxLearningPathDTO.Edge(getLectureUnitOutEdgeId(competency.getId(), lectureUnit.getId()), getLectureUnitNodeId(competency.getId(), lectureUnit.getId()), + endNodeId)); }); // generate nodes and edges for exercises competency.getExercises().forEach(exercise -> { - currentCluster.add(new NgxLearningPathDTO.Node(prefix + exercise.getId(), NgxLearningPathDTO.NodeType.EXERCISE, exercise.getId(), exercise.getTitle())); - edges.add(new NgxLearningPathDTO.Edge(prefix + exercise.getId() + "-in", startNodeId, prefix + exercise.getId())); - edges.add(new NgxLearningPathDTO.Edge(prefix + exercise.getId() + "-out", prefix + exercise.getId(), endNodeId)); + currentCluster.add(new NgxLearningPathDTO.Node(getExerciseNodeId(competency.getId(), exercise.getId()), NgxLearningPathDTO.NodeType.EXERCISE, exercise.getId(), + exercise.getTitle())); + edges.add(new NgxLearningPathDTO.Edge(getExerciseInEdgeId(competency.getId(), exercise.getId()), startNodeId, getExerciseNodeId(competency.getId(), exercise.getId()))); + edges.add(new NgxLearningPathDTO.Edge(getExerciseOutEdgeId(competency.getId(), exercise.getId()), getExerciseNodeId(competency.getId(), exercise.getId()), endNodeId)); }); // if no linked learning units exist directly link start to end if (currentCluster.size() == 2) { - edges.add(new NgxLearningPathDTO.Edge(prefix, startNodeId, endNodeId)); + edges.add(new NgxLearningPathDTO.Edge(getDirectEdgeId(competency.getId()), startNodeId, endNodeId)); } // generate cluster for competency var childNodeIds = currentCluster.stream().map(NgxLearningPathDTO.Node::id).collect(Collectors.toSet()); childNodeIds.add(startNodeId); childNodeIds.add(endNodeId); - clusters.add(new NgxLearningPathDTO.Cluster("" + competency.getId(), competency.getTitle(), childNodeIds)); + clusters.add(new NgxLearningPathDTO.Cluster(String.valueOf(competency.getId()), competency.getTitle(), childNodeIds)); nodes.addAll(currentCluster); } @@ -257,28 +261,29 @@ private void generateNgxRepresentationForRelations(LearningPath learningPath, Se final var competenciesInMatchRelation = relations.stream().filter(relation -> relation.getType().equals(CompetencyRelation.RelationType.MATCHES)) .flatMap(relation -> Stream.of(relation.getHeadCompetency().getId(), relation.getTailCompetency().getId())).collect(Collectors.toSet()); if (!competenciesInMatchRelation.isEmpty()) { - UnionFind matchClusters = new UnionFind(competenciesInMatchRelation); + UnionFind matchClusters = new UnionFind<>(competenciesInMatchRelation); relations.stream().filter(relation -> relation.getType().equals(CompetencyRelation.RelationType.MATCHES)) .forEach(relation -> matchClusters.union(relation.getHeadCompetency().getId(), relation.getTailCompetency().getId())); + // generate map between competencies and cluster node AtomicInteger matchClusterId = new AtomicInteger(); relations.stream().filter(relation -> relation.getType().equals(CompetencyRelation.RelationType.MATCHES)) .flatMapToLong(relation -> LongStream.of(relation.getHeadCompetency().getId(), relation.getTailCompetency().getId())).distinct().forEach(competencyId -> { var parentId = matchClusters.find(competencyId); - var clusterId = competencyToMatchCluster.computeIfAbsent((Long) parentId, (key) -> matchClusterId.getAndIncrement()); + var clusterId = competencyToMatchCluster.computeIfAbsent(parentId, (key) -> matchClusterId.getAndIncrement()); competencyToMatchCluster.put(competencyId, clusterId); }); // generate match cluster start and end nodes for (int i = 0; i < matchClusters.numberOfSets(); i++) { - nodes.add(new NgxLearningPathDTO.Node("matching-" + i + "-start", NgxLearningPathDTO.NodeType.COMPETENCY_START, -1, "")); - nodes.add(new NgxLearningPathDTO.Node("matching-" + i + "-end", NgxLearningPathDTO.NodeType.COMPETENCY_END, -1, "")); + nodes.add(new NgxLearningPathDTO.Node(getMatchingClusterStartNodeId(i), NgxLearningPathDTO.NodeType.COMPETENCY_START, -1, "")); + nodes.add(new NgxLearningPathDTO.Node(getMatchingClusterEndNodeId(i), NgxLearningPathDTO.NodeType.COMPETENCY_END, -1, "")); } // generate edges between match cluster nodes and corresponding competencies - competencyToMatchCluster.entrySet().stream().forEach(entry -> { - edges.add(new NgxLearningPathDTO.Edge(entry.getKey() + "-in", "matching-" + entry.getValue() + "-start", entry.getKey() + "-start")); - edges.add(new NgxLearningPathDTO.Edge(entry.getKey() + "-out", entry.getKey() + "-end", "matching-" + entry.getValue() + "-end")); + competencyToMatchCluster.forEach((competency, cluster) -> { + edges.add(new NgxLearningPathDTO.Edge(getInEdgeId(competency), getMatchingClusterStartNodeId(cluster), getCompetencyStartNodeId(competency))); + edges.add(new NgxLearningPathDTO.Edge(getOutEdgeId(competency), getCompetencyEndNodeId(competency), getMatchingClusterEndNodeId(cluster))); }); } @@ -299,10 +304,12 @@ private void generateNgxRepresentationForRelations(LearningPath learningPath, Se private void generateNgxRepresentationForRelation(CompetencyRelation relation, Map competencyToMatchCluster, Set createdRelations, Set edges) { final var sourceId = relation.getHeadCompetency().getId(); - final String sourceNodeId = competencyToMatchCluster.containsKey(sourceId) ? "matching-" + competencyToMatchCluster.get(sourceId) + "-end" : sourceId + "-end"; + final String sourceNodeId = competencyToMatchCluster.containsKey(sourceId) ? getMatchingClusterEndNodeId(competencyToMatchCluster.get(sourceId)) + : getCompetencyEndNodeId(sourceId); final var targetId = relation.getTailCompetency().getId(); - final String targetNodeId = competencyToMatchCluster.containsKey(targetId) ? "matching-" + competencyToMatchCluster.get(targetId) + "-start" : sourceId + "-start"; - final String relationEdgeId = "relation-" + sourceNodeId + "-" + targetNodeId; + final String targetNodeId = competencyToMatchCluster.containsKey(targetId) ? getMatchingClusterStartNodeId(competencyToMatchCluster.get(targetId)) + : getCompetencyStartNodeId(targetId); + final String relationEdgeId = getRelationEdgeId(sourceNodeId, targetNodeId); // skip if relation has already been created (possible for edges linked to matching cluster start/end nodes) if (!createdRelations.contains(relationEdgeId)) { final var edge = new NgxLearningPathDTO.Edge(relationEdgeId, sourceNodeId, targetNodeId); @@ -310,4 +317,60 @@ private void generateNgxRepresentationForRelation(CompetencyRelation relation, M createdRelations.add(relationEdgeId); } } + + public static String getCompetencyStartNodeId(long competencyId) { + return competencyId + "-start"; + } + + public static String getCompetencyEndNodeId(long competencyId) { + return competencyId + "-end"; + } + + public static String getLectureUnitNodeId(long competencyId, long lectureUnitId) { + return competencyId + "-lu-" + lectureUnitId; + } + + public static String getExerciseNodeId(long competencyId, long exerciseId) { + return competencyId + "-ex-" + exerciseId; + } + + public static String getMatchingClusterStartNodeId(long matchingClusterId) { + return "matching-" + matchingClusterId + "-start"; + } + + public static String getMatchingClusterEndNodeId(long matchingClusterId) { + return "matching-" + matchingClusterId + "-end"; + } + + public static String getLectureUnitInEdgeId(long competencyId, long lectureUnitId) { + return competencyId + "-lu-" + getInEdgeId(lectureUnitId); + } + + public static String getLectureUnitOutEdgeId(long competencyId, long lectureUnitId) { + return competencyId + "-lu-" + getOutEdgeId(lectureUnitId); + } + + public static String getExerciseInEdgeId(long competencyId, long exercise) { + return competencyId + "-ex-" + getInEdgeId(exercise); + } + + public static String getExerciseOutEdgeId(long competencyId, long exercise) { + return competencyId + "-ex-" + getOutEdgeId(exercise); + } + + public static String getInEdgeId(long id) { + return id + "-in"; + } + + public static String getOutEdgeId(long id) { + return id + "-out"; + } + + public static String getRelationEdgeId(String sourceNodeId, String targetNodeId) { + return "relation-" + sourceNodeId + "-" + targetNodeId; + } + + public static String getDirectEdgeId(long competencyId) { + return competencyId + "-direct"; + } } diff --git a/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java b/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java index fbe30bd68071..6451d226004a 100644 --- a/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java @@ -4,8 +4,11 @@ import org.springframework.stereotype.Service; import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.Exercise; import de.tum.in.www1.artemis.domain.competency.Competency; -import de.tum.in.www1.artemis.repository.CompetencyRepository; +import de.tum.in.www1.artemis.domain.competency.CompetencyRelation; +import de.tum.in.www1.artemis.domain.lecture.LectureUnit; +import de.tum.in.www1.artemis.repository.*; /** * Service responsible for initializing the database with specific testdata related to competencies for use in integration tests. @@ -16,6 +19,15 @@ public class CompetencyUtilService { @Autowired private CompetencyRepository competencyRepo; + @Autowired + private LectureUnitRepository lectureUnitRepository; + + @Autowired + private ExerciseRepository exerciseRepository; + + @Autowired + private CompetencyRelationRepository competencyRelationRepository; + /** * Creates competency and links it to the course. The title of the competency will hold the specified suffix. * @@ -42,7 +54,7 @@ public Competency createCompetency(Course course) { } /** - * Creates multiple competencies and links them to the course + * Creates multiple competencies and links them to the course. * * @param course course the competencies will be linked to * @param numberOfCompetencies number of competencies to create @@ -55,4 +67,34 @@ public Competency[] createCompetencies(Course course, int numberOfCompetencies) } return competencies; } + + /** + * Link lecture unit to competency. + * + * @param competency the competency to link the learning unit to + * @param lectureUnit the lecture unit that will be linked to the competency + */ + public void linkLectureUnitToCompetency(Competency competency, LectureUnit lectureUnit) { + lectureUnit.getCompetencies().add(competency); + lectureUnitRepository.save(lectureUnit); + } + + /** + * Link exercise to competency. + * + * @param competency the competency to link the learning unit to + * @param exercise the exercise that will be linked to the competency + */ + public void linkExerciseToCompetency(Competency competency, Exercise exercise) { + exercise.getCompetencies().add(competency); + exerciseRepository.save(exercise); + } + + public void addRelation(Competency tail, CompetencyRelation.RelationType type, Competency head) { + CompetencyRelation relation = new CompetencyRelation(); + relation.setTailCompetency(tail); + relation.setHeadCompetency(head); + relation.setType(type); + competencyRelationRepository.save(relation); + } } diff --git a/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java b/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java index e05ba4c31dfb..658848ba26c2 100644 --- a/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java @@ -1,10 +1,16 @@ package de.tum.in.www1.artemis.competency; +import java.util.Set; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.competency.Competency; +import de.tum.in.www1.artemis.domain.competency.LearningPath; +import de.tum.in.www1.artemis.repository.CompetencyRepository; import de.tum.in.www1.artemis.repository.CourseRepository; +import de.tum.in.www1.artemis.repository.LearningPathRepository; import de.tum.in.www1.artemis.service.LearningPathService; /** @@ -19,6 +25,12 @@ public class LearningPathUtilService { @Autowired LearningPathService learningPathService; + @Autowired + LearningPathRepository learningPathRepository; + + @Autowired + CompetencyRepository competencyRepository; + /** * Enable and generate learning paths for course. * @@ -32,4 +44,16 @@ public Course enableAndGenerateLearningPathsForCourse(Course course) { return courseRepository.save(course); } + public LearningPath createLearningPathInCourse(Course course) { + final var competencies = competencyRepository.findAllForCourse(course.getId()); + LearningPath learningPath = createLearningPath(competencies); + learningPath.setCourse(course); + return learningPathRepository.save(learningPath); + } + + public LearningPath createLearningPath(Set competencies) { + LearningPath lp = new LearningPath(); + lp.setCompetencies(competencies); + return learningPathRepository.save(lp); + } } diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LectureUtilService.java b/src/test/java/de/tum/in/www1/artemis/lecture/LectureUtilService.java index 2f61c50ae550..a933f871d7e9 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LectureUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LectureUtilService.java @@ -145,6 +145,7 @@ public AttachmentUnit createAttachmentUnitWithSlides(int numberOfSlides) { public TextUnit createTextUnit() { TextUnit textUnit = new TextUnit(); + textUnit.setName("Name Lorem Ipsum"); textUnit.setContent("Lorem Ipsum"); return textUnitRepository.save(textUnit); } diff --git a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java new file mode 100644 index 000000000000..b5280eafd594 --- /dev/null +++ b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java @@ -0,0 +1,247 @@ +package de.tum.in.www1.artemis.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.competency.CompetencyUtilService; +import de.tum.in.www1.artemis.competency.LearningPathUtilService; +import de.tum.in.www1.artemis.course.CourseUtilService; +import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.Exercise; +import de.tum.in.www1.artemis.domain.competency.Competency; +import de.tum.in.www1.artemis.domain.competency.CompetencyRelation; +import de.tum.in.www1.artemis.domain.competency.LearningPath; +import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; +import de.tum.in.www1.artemis.domain.lecture.LectureUnit; +import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.lecture.LectureUtilService; +import de.tum.in.www1.artemis.repository.CompetencyRepository; +import de.tum.in.www1.artemis.repository.LearningPathRepository; +import de.tum.in.www1.artemis.security.SecurityUtils; +import de.tum.in.www1.artemis.web.rest.dto.learningpath.NgxLearningPathDTO; + +public class LearningPathServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { + + @Autowired + LearningPathService learningPathService; + + @Autowired + LearningPathUtilService learningPathUtilService; + + @Autowired + CourseUtilService courseUtilService; + + @Autowired + CompetencyUtilService competencyUtilService; + + @Autowired + LearningPathRepository learningPathRepository; + + @Autowired + LectureUtilService lectureUtilService; + + @Autowired + ProgrammingExerciseUtilService programmingExerciseUtilService; + + @Autowired + CompetencyRepository competencyRepository; + + private Course course; + + private void generateAndAssert(NgxLearningPathDTO expected) { + LearningPath learningPath = learningPathUtilService.createLearningPathInCourse(course); + learningPath = learningPathRepository.findWithEagerCompetenciesAndLearningUnitsByIdElseThrow(learningPath.getId()); + NgxLearningPathDTO actual = learningPathService.generateNgxRepresentation(learningPath); + assertThat(actual).isNotNull(); + assertNgxRepEquals(actual, expected); + } + + private void assertNgxRepEquals(NgxLearningPathDTO was, NgxLearningPathDTO expected) { + assertThat(was.nodes()).as("correct nodes").containsExactlyInAnyOrderElementsOf(expected.nodes()); + assertThat(was.edges()).as("correct edges").containsExactlyInAnyOrderElementsOf(expected.edges()); + assertThat(was.clusters()).as("correct clusters").containsExactlyInAnyOrderElementsOf(expected.clusters()); + } + + @BeforeEach + void setup() { + course = courseUtilService.createCourse(); + } + + @Nested + class GenerateNgxRepresentationBaseTest { + + @BeforeEach + void setAuthorizationForRepositoryRequests() { + SecurityUtils.setAuthorizationObject(); + } + + @Test + void testEmptyLearningPath() { + NgxLearningPathDTO expected = new NgxLearningPathDTO(Set.of(), Set.of(), Set.of()); + generateAndAssert(expected); + } + + @Test + void testEmptyCompetency() { + final var competency = competencyUtilService.createCompetency(course); + final var startNodeId = LearningPathService.getCompetencyStartNodeId(competency.getId()); + final var endNodeId = LearningPathService.getCompetencyEndNodeId(competency.getId()); + Set expectedNodes = getExpectedNodesOfEmptyCompetency(competency); + Set expectedEdges = Set.of(new NgxLearningPathDTO.Edge(LearningPathService.getDirectEdgeId(competency.getId()), startNodeId, endNodeId)); + Set expectedClusters = Set + .of(new NgxLearningPathDTO.Cluster(String.valueOf(competency.getId()), competency.getTitle(), Set.of(startNodeId, endNodeId))); + NgxLearningPathDTO expected = new NgxLearningPathDTO(expectedNodes, expectedEdges, expectedClusters); + generateAndAssert(expected); + } + + @Test + void testCompetencyWithLectureUnitAndExercise() { + var competency = competencyUtilService.createCompetency(course); + final var lectureUnit = lectureUtilService.createTextUnit(); + competencyUtilService.linkLectureUnitToCompetency(competency, lectureUnit); + final var exercise = programmingExerciseUtilService.addProgrammingExerciseToCourse(course, false, false, ProgrammingLanguage.JAVA, "Some Title", "someshortname"); + competencyUtilService.linkExerciseToCompetency(competency, exercise); + final var startNodeId = LearningPathService.getCompetencyStartNodeId(competency.getId()); + final var endNodeId = LearningPathService.getCompetencyEndNodeId(competency.getId()); + HashSet expectedNodes = getExpectedNodesOfEmptyCompetency(competency); + expectedNodes.add(getNodeForLectureUnit(competency, lectureUnit)); + expectedNodes.add(getNodeForExercise(competency, exercise)); + Set expectedEdges = Set.of( + new NgxLearningPathDTO.Edge(LearningPathService.getLectureUnitInEdgeId(competency.getId(), lectureUnit.getId()), startNodeId, + LearningPathService.getLectureUnitNodeId(competency.getId(), lectureUnit.getId())), + new NgxLearningPathDTO.Edge(LearningPathService.getLectureUnitOutEdgeId(competency.getId(), lectureUnit.getId()), + LearningPathService.getLectureUnitNodeId(competency.getId(), lectureUnit.getId()), endNodeId), + new NgxLearningPathDTO.Edge(LearningPathService.getExerciseInEdgeId(competency.getId(), exercise.getId()), startNodeId, + LearningPathService.getExerciseNodeId(competency.getId(), lectureUnit.getId())), + new NgxLearningPathDTO.Edge(LearningPathService.getExerciseOutEdgeId(competency.getId(), exercise.getId()), + LearningPathService.getExerciseNodeId(competency.getId(), exercise.getId()), endNodeId)); + Set expectedClusters = Set.of(new NgxLearningPathDTO.Cluster(String.valueOf(competency.getId()), competency.getTitle(), + expectedNodes.stream().map(NgxLearningPathDTO.Node::id).collect(Collectors.toSet()))); + NgxLearningPathDTO expected = new NgxLearningPathDTO(expectedNodes, expectedEdges, expectedClusters); + generateAndAssert(expected); + } + + @Test + void testMultipleCompetencies() { + Competency[] competencies = { competencyUtilService.createCompetency(course), competencyUtilService.createCompetency(course), + competencyUtilService.createCompetency(course) }; + String[] startNodeIds = Arrays.stream(competencies).map(Competency::getId).map(LearningPathService::getCompetencyStartNodeId).toArray(String[]::new); + String[] endNodeIds = Arrays.stream(competencies).map(Competency::getId).map(LearningPathService::getCompetencyEndNodeId).toArray(String[]::new); + Set expectedNodes = new HashSet<>(); + Set expectedEdges = new HashSet<>(); + Set expectedClusters = new HashSet<>(); + for (int i = 0; i < competencies.length; i++) { + expectedNodes.addAll(getExpectedNodesOfEmptyCompetency(competencies[i])); + expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getDirectEdgeId(competencies[i].getId()), startNodeIds[i], endNodeIds[i])); + expectedClusters.add(new NgxLearningPathDTO.Cluster(String.valueOf(competencies[i].getId()), competencies[i].getTitle(), Set.of(startNodeIds[i], endNodeIds[i]))); + } + NgxLearningPathDTO expected = new NgxLearningPathDTO(expectedNodes, expectedEdges, expectedClusters); + generateAndAssert(expected); + } + } + + @Nested + class GenerateNgxRepresentationRelationTest { + + private Competency competency1; + + private Competency competency2; + + private Set expectedNodes; + + Set expectedEdges; + + Set expectedClusters; + + @BeforeEach + void setAuthorizationForRepositoryRequests() { + SecurityUtils.setAuthorizationObject(); + } + + @BeforeEach + void setup() { + competency1 = competencyUtilService.createCompetency(course); + competency2 = competencyUtilService.createCompetency(course); + expectedNodes = new HashSet<>(); + expectedNodes.addAll(getExpectedNodesOfEmptyCompetency(competency1)); + expectedNodes.addAll(getExpectedNodesOfEmptyCompetency(competency2)); + expectedEdges = new HashSet<>(); + expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getDirectEdgeId(competency1.getId()), + LearningPathService.getCompetencyStartNodeId(competency1.getId()), LearningPathService.getCompetencyEndNodeId(competency1.getId()))); + expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getDirectEdgeId(competency2.getId()), + LearningPathService.getCompetencyStartNodeId(competency2.getId()), LearningPathService.getCompetencyEndNodeId(competency2.getId()))); + expectedClusters = new HashSet<>(); + expectedClusters.add(new NgxLearningPathDTO.Cluster(String.valueOf(competency1.getId()), competency1.getTitle(), + Set.of(LearningPathService.getCompetencyStartNodeId(competency1.getId()), LearningPathService.getCompetencyEndNodeId(competency1.getId())))); + expectedClusters.add(new NgxLearningPathDTO.Cluster(String.valueOf(competency2.getId()), competency2.getTitle(), + Set.of(LearningPathService.getCompetencyStartNodeId(competency2.getId()), LearningPathService.getCompetencyEndNodeId(competency2.getId())))); + } + + void testSimpleRelation(CompetencyRelation.RelationType type) { + competencyUtilService.addRelation(competency1, type, competency2); + final var sourceNodeId = LearningPathService.getCompetencyEndNodeId(competency2.getId()); + final var targetNodeId = LearningPathService.getCompetencyStartNodeId(competency1.getId()); + expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getRelationEdgeId(sourceNodeId, targetNodeId), sourceNodeId, targetNodeId)); + NgxLearningPathDTO expected = new NgxLearningPathDTO(expectedNodes, expectedEdges, expectedClusters); + generateAndAssert(expected); + } + + @Test + void testSingleRelates() { + testSimpleRelation(CompetencyRelation.RelationType.RELATES); + } + + @Test + void testSingleAssumes() { + testSimpleRelation(CompetencyRelation.RelationType.ASSUMES); + } + + @Test + void testSingleExtends() { + testSimpleRelation(CompetencyRelation.RelationType.EXTENDS); + } + + @Test + void testSingleMatches() { + competencyUtilService.addRelation(competency1, CompetencyRelation.RelationType.MATCHES, competency2); + expectedNodes.add(new NgxLearningPathDTO.Node(LearningPathService.getMatchingClusterStartNodeId(0), NgxLearningPathDTO.NodeType.COMPETENCY_START, -1, "")); + expectedNodes.add(new NgxLearningPathDTO.Node(LearningPathService.getMatchingClusterEndNodeId(0), NgxLearningPathDTO.NodeType.COMPETENCY_END, -1, "")); + expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getInEdgeId(competency1.getId()), LearningPathService.getMatchingClusterStartNodeId(0), + LearningPathService.getCompetencyStartNodeId(competency1.getId()))); + expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getOutEdgeId(competency1.getId()), LearningPathService.getCompetencyEndNodeId(competency1.getId()), + LearningPathService.getMatchingClusterEndNodeId(0))); + expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getInEdgeId(competency2.getId()), LearningPathService.getMatchingClusterStartNodeId(0), + LearningPathService.getCompetencyStartNodeId(competency2.getId()))); + expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getOutEdgeId(competency2.getId()), LearningPathService.getCompetencyEndNodeId(competency2.getId()), + LearningPathService.getMatchingClusterEndNodeId(0))); + NgxLearningPathDTO expected = new NgxLearningPathDTO(expectedNodes, expectedEdges, expectedClusters); + generateAndAssert(expected); + } + } + + private static HashSet getExpectedNodesOfEmptyCompetency(Competency competency) { + return new HashSet<>(Set.of( + new NgxLearningPathDTO.Node(LearningPathService.getCompetencyStartNodeId(competency.getId()), NgxLearningPathDTO.NodeType.COMPETENCY_START, competency.getId(), ""), + new NgxLearningPathDTO.Node(LearningPathService.getCompetencyEndNodeId(competency.getId()), NgxLearningPathDTO.NodeType.COMPETENCY_END, competency.getId(), ""))); + } + + private static NgxLearningPathDTO.Node getNodeForLectureUnit(Competency competency, LectureUnit lectureUnit) { + return new NgxLearningPathDTO.Node(LearningPathService.getLectureUnitNodeId(competency.getId(), lectureUnit.getId()), NgxLearningPathDTO.NodeType.LECTURE_UNIT, + lectureUnit.getId(), lectureUnit.getName()); + } + + private static NgxLearningPathDTO.Node getNodeForExercise(Competency competency, Exercise exercise) { + return new NgxLearningPathDTO.Node(LearningPathService.getExerciseNodeId(competency.getId(), exercise.getId()), NgxLearningPathDTO.NodeType.EXERCISE, exercise.getId(), + exercise.getTitle()); + } +} From 3a399b1e95b2487d5e71a3cefb5280f6af992d55 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Fri, 7 Jul 2023 16:10:22 +0200 Subject: [PATCH 047/215] fix code style --- .../dto/learningpath/NgxLearningPathDTO.java | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/NgxLearningPathDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/NgxLearningPathDTO.java index 4b82c9537083..08606e88c07f 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/NgxLearningPathDTO.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/NgxLearningPathDTO.java @@ -5,18 +5,16 @@ import com.fasterxml.jackson.annotation.JsonInclude; /** - * Represents simplified + * Represents simplified learning path optimized for Ngx representation */ @JsonInclude(JsonInclude.Include.NON_EMPTY) public record NgxLearningPathDTO(Set nodes, Set edges, Set clusters) { @Override public boolean equals(Object obj) { - if (!(obj instanceof NgxLearningPathDTO)) { + if (!(obj instanceof NgxLearningPathDTO other)) { return false; } - - final NgxLearningPathDTO other = (NgxLearningPathDTO) obj; return nodes.equals(other.nodes) && edges.equals(other.edges) && clusters.equals(other.clusters); } @@ -29,11 +27,9 @@ public record Node(String id, NodeType type, long linkedResource, String label) @Override public boolean equals(Object obj) { - if (!(obj instanceof Node)) { + if (!(obj instanceof Node other)) { return false; } - - final Node other = (Node) obj; return id.equals(other.id) && type.equals(other.type) && linkedResource == other.linkedResource && label.equals(other.label); } @@ -47,11 +43,9 @@ public record Edge(String id, String source, String target) { @Override public boolean equals(Object obj) { - if (!(obj instanceof Edge)) { + if (!(obj instanceof Edge other)) { return false; } - - final Edge other = (Edge) obj; return id.equals(other.id) && source.equals(other.source) && target.equals(other.target); } @@ -65,10 +59,9 @@ public record Cluster(String id, String label, Set childNodeIds) { @Override public boolean equals(Object obj) { - if (!(obj instanceof Cluster)) { + if (!(obj instanceof Cluster other)) { return false; } - final Cluster other = (Cluster) obj; return id.equals(other.id) && label.equals(other.label) && childNodeIds.equals(other.childNodeIds); } From 7de2548825317d674e0d5d427d1ce25b6008a17f Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Sat, 8 Jul 2023 15:32:43 +0200 Subject: [PATCH 048/215] Fix doc comment --- .../de/tum/in/www1/artemis/web/rest/LearningPathResource.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java index f37c31f7b7e1..44f068602317 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java @@ -69,6 +69,7 @@ public ResponseEntity enableLearningPathsForCourse(@PathVariable Long co * GET /courses/:courseId/learning-paths : Gets all the learning paths of a course. The result is pageable. * * @param courseId the id of the course for which the learning paths should be fetched + * @param search the pageable search containing the page size, page number and query string * @return the ResponseEntity with status 200 (OK) and with body the desired page, sorted and matching the given query */ @GetMapping("/courses/{courseId}/learning-paths") From 235374b1eb9f82af988e253855a7ab33f2c35883 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Sat, 8 Jul 2023 15:53:10 +0200 Subject: [PATCH 049/215] Add new test case and improve readability --- .../service/LearningPathServiceTest.java | 46 +++++++++++++++---- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java index b5280eafd594..1451542984f1 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java @@ -173,18 +173,9 @@ void setup() { competency1 = competencyUtilService.createCompetency(course); competency2 = competencyUtilService.createCompetency(course); expectedNodes = new HashSet<>(); - expectedNodes.addAll(getExpectedNodesOfEmptyCompetency(competency1)); - expectedNodes.addAll(getExpectedNodesOfEmptyCompetency(competency2)); expectedEdges = new HashSet<>(); - expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getDirectEdgeId(competency1.getId()), - LearningPathService.getCompetencyStartNodeId(competency1.getId()), LearningPathService.getCompetencyEndNodeId(competency1.getId()))); - expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getDirectEdgeId(competency2.getId()), - LearningPathService.getCompetencyStartNodeId(competency2.getId()), LearningPathService.getCompetencyEndNodeId(competency2.getId()))); expectedClusters = new HashSet<>(); - expectedClusters.add(new NgxLearningPathDTO.Cluster(String.valueOf(competency1.getId()), competency1.getTitle(), - Set.of(LearningPathService.getCompetencyStartNodeId(competency1.getId()), LearningPathService.getCompetencyEndNodeId(competency1.getId())))); - expectedClusters.add(new NgxLearningPathDTO.Cluster(String.valueOf(competency2.getId()), competency2.getTitle(), - Set.of(LearningPathService.getCompetencyStartNodeId(competency2.getId()), LearningPathService.getCompetencyEndNodeId(competency2.getId())))); + addExpectedComponentsForEmptyCompetencies(competency1, competency2); } void testSimpleRelation(CompetencyRelation.RelationType type) { @@ -227,6 +218,41 @@ void testSingleMatches() { NgxLearningPathDTO expected = new NgxLearningPathDTO(expectedNodes, expectedEdges, expectedClusters); generateAndAssert(expected); } + + @Test + void testMatchesTransitive() { + var competency3 = competencyUtilService.createCompetency(course); + addExpectedComponentsForEmptyCompetencies(competency3); + + competencyUtilService.addRelation(competency1, CompetencyRelation.RelationType.MATCHES, competency2); + competencyUtilService.addRelation(competency2, CompetencyRelation.RelationType.MATCHES, competency3); + expectedNodes.add(new NgxLearningPathDTO.Node(LearningPathService.getMatchingClusterStartNodeId(0), NgxLearningPathDTO.NodeType.COMPETENCY_START, -1, "")); + expectedNodes.add(new NgxLearningPathDTO.Node(LearningPathService.getMatchingClusterEndNodeId(0), NgxLearningPathDTO.NodeType.COMPETENCY_END, -1, "")); + expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getInEdgeId(competency1.getId()), LearningPathService.getMatchingClusterStartNodeId(0), + LearningPathService.getCompetencyStartNodeId(competency1.getId()))); + expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getOutEdgeId(competency1.getId()), LearningPathService.getCompetencyEndNodeId(competency1.getId()), + LearningPathService.getMatchingClusterEndNodeId(0))); + expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getInEdgeId(competency2.getId()), LearningPathService.getMatchingClusterStartNodeId(0), + LearningPathService.getCompetencyStartNodeId(competency2.getId()))); + expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getOutEdgeId(competency2.getId()), LearningPathService.getCompetencyEndNodeId(competency2.getId()), + LearningPathService.getMatchingClusterEndNodeId(0))); + expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getInEdgeId(competency3.getId()), LearningPathService.getMatchingClusterStartNodeId(0), + LearningPathService.getCompetencyStartNodeId(competency3.getId()))); + expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getOutEdgeId(competency3.getId()), LearningPathService.getCompetencyEndNodeId(competency3.getId()), + LearningPathService.getMatchingClusterEndNodeId(0))); + NgxLearningPathDTO expected = new NgxLearningPathDTO(expectedNodes, expectedEdges, expectedClusters); + generateAndAssert(expected); + } + + private void addExpectedComponentsForEmptyCompetencies(Competency... competencies) { + for (var competency : competencies) { + expectedNodes.addAll(getExpectedNodesOfEmptyCompetency(competency)); + expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getDirectEdgeId(competency.getId()), + LearningPathService.getCompetencyStartNodeId(competency.getId()), LearningPathService.getCompetencyEndNodeId(competency.getId()))); + expectedClusters.add(new NgxLearningPathDTO.Cluster(String.valueOf(competency.getId()), competency.getTitle(), + Set.of(LearningPathService.getCompetencyStartNodeId(competency.getId()), LearningPathService.getCompetencyEndNodeId(competency.getId())))); + } + } } private static HashSet getExpectedNodesOfEmptyCompetency(Competency competency) { From 3d6ddfbffcbe5a8dfb98486cb75dc0eeb0dd2d2b Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Fri, 14 Jul 2023 01:49:13 +0200 Subject: [PATCH 050/215] add LP view excluding exercises --- .../repository/LearningPathRepository.java | 8 ++ .../repository/LectureUnitRepository.java | 13 +++ .../artemis/service/LearningPathService.java | 20 ++++- .../web/rest/LearningPathResource.java | 43 +++++++++- .../learning-path-graph-node.component.html | 4 + .../learning-path-graph-node.component.ts | 24 ++++++ .../learning-path-graph.component.html | 46 ++++++++++ .../learning-path-graph.component.scss | 10 +++ .../learning-path-graph.component.ts | 56 ++++++++++++ .../learning-paths/learning-path.service.ts | 19 ++++ .../learning-paths/learning-paths.module.ts | 42 +++++++-- .../learning-path-container.component.html | 18 ++++ .../learning-path-container.component.ts | 65 ++++++++++++++ ...learning-path-graph-sidebar.component.html | 31 +++++++ ...learning-path-graph-sidebar.component.scss | 86 +++++++++++++++++++ .../learning-path-graph-sidebar.component.ts | 53 ++++++++++++ ...ning-path-lecture-unit-view.component.html | 6 ++ ...arning-path-lecture-unit-view.component.ts | 38 ++++++++ .../learning-path-lecture-unit-view.module.ts | 11 +++ .../app/entities/learning-path.model.ts | 34 ++++++++ .../course-learning-path.component.html | 1 + .../course-learning-path.component.ts | 20 +++++ .../course-learning-path.module.ts | 28 ++++++ .../overview/course-overview.component.html | 10 +++ .../app/overview/courses-routing.module.ts | 4 + .../shared/layouts/navbar/navbar.component.ts | 2 + src/main/webapp/i18n/de/competency.json | 5 ++ src/main/webapp/i18n/de/global.json | 1 + .../webapp/i18n/de/student-dashboard.json | 1 + src/main/webapp/i18n/en/competency.json | 5 ++ src/main/webapp/i18n/en/global.json | 1 + .../webapp/i18n/en/student-dashboard.json | 1 + .../lecture/LearningPathIntegrationTest.java | 20 +++++ 33 files changed, 719 insertions(+), 7 deletions(-) create mode 100644 src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.html create mode 100644 src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.ts create mode 100644 src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.html create mode 100644 src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.scss create mode 100644 src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts create mode 100644 src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html create mode 100644 src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts create mode 100644 src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.html create mode 100644 src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.scss create mode 100644 src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.ts create mode 100644 src/main/webapp/app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.component.html create mode 100644 src/main/webapp/app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.component.ts create mode 100644 src/main/webapp/app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.module.ts create mode 100644 src/main/webapp/app/overview/course-learning-path/course-learning-path.component.html create mode 100644 src/main/webapp/app/overview/course-learning-path/course-learning-path.component.ts create mode 100644 src/main/webapp/app/overview/course-learning-path/course-learning-path.module.ts diff --git a/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java index 1957bc915992..54cbec29d067 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java @@ -30,6 +30,14 @@ default LearningPath findWithEagerCompetenciesByCourseIdAndUserIdElseThrow(long return findWithEagerCompetenciesByCourseIdAndUserId(courseId, userId).orElseThrow(() -> new EntityNotFoundException("LearningPath")); } + @EntityGraph(type = LOAD, attributePaths = { "competencies", "competencies.lectureUnits", "competencies.exercises" }) + Optional findWithEagerCompetenciesAndLearningUnitsByCourseIdAndUserId(long courseId, long userId); + + @NotNull + default LearningPath findWithEagerCompetenciesAndLearningUnitsByCourseIdAndUserIdElseThrow(long courseId, long userId) { + return findWithEagerCompetenciesAndLearningUnitsByCourseIdAndUserId(courseId, userId).orElseThrow(() -> new EntityNotFoundException("LearningPath")); + } + @Query(""" SELECT lp FROM LearningPath lp diff --git a/src/main/java/de/tum/in/www1/artemis/repository/LectureUnitRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/LectureUnitRepository.java index 37142b28d9ee..f83e7521ee83 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/LectureUnitRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/LectureUnitRepository.java @@ -1,8 +1,13 @@ package de.tum.in.www1.artemis.repository; +import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.LOAD; + import java.util.Optional; import java.util.Set; +import javax.validation.constraints.NotNull; + +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -56,4 +61,12 @@ default LectureUnit findByIdWithCompetenciesBidirectionalElseThrow(long lectureU default LectureUnit findByIdWithCompetenciesElseThrow(long lectureUnitId) { return findByIdWithCompetencies(lectureUnitId).orElseThrow(() -> new EntityNotFoundException("LectureUnit", lectureUnitId)); } + + @EntityGraph(type = LOAD, attributePaths = { "completedUsers" }) + Optional findWithEagerCompletedUsersById(long lectureUnitId); + + @NotNull + default LectureUnit findWithEagerCompletedUsersByIdElseThrow(long lectureUnitId) { + return findWithEagerCompletedUsersById(lectureUnitId).orElseThrow(() -> new EntityNotFoundException("LectureUnit", lectureUnitId)); + } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index 6b9e43aa1f42..3995bd50f897 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -15,6 +15,7 @@ import org.springframework.stereotype.Service; import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.LearningObject; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.competency.CompetencyRelation; @@ -40,13 +41,20 @@ public class LearningPathService { private final CompetencyRelationRepository competencyRelationRepository; + private final LectureUnitRepository lectureUnitRepository; + + private final ExerciseRepository exerciseRepository; + public LearningPathService(UserRepository userRepository, LearningPathRepository learningPathRepository, CompetencyProgressRepository competencyProgressRepository, - CourseRepository courseRepository, CompetencyRelationRepository competencyRelationRepository) { + CourseRepository courseRepository, CompetencyRelationRepository competencyRelationRepository, LectureUnitRepository lectureUnitRepository, + ExerciseRepository exerciseRepository) { this.userRepository = userRepository; this.learningPathRepository = learningPathRepository; this.competencyProgressRepository = competencyProgressRepository; this.courseRepository = courseRepository; this.competencyRelationRepository = competencyRelationRepository; + this.lectureUnitRepository = lectureUnitRepository; + this.exerciseRepository = exerciseRepository; } /** @@ -373,4 +381,14 @@ public static String getRelationEdgeId(String sourceNodeId, String targetNodeId) public static String getDirectEdgeId(long competencyId) { return competencyId + "-direct"; } + + public LearningObject getRecommendation(LearningPath learningPath) { + return learningPath.getCompetencies().stream() + .flatMap(competency -> Stream.concat( + competency.getLectureUnits().stream() + .filter(lectureUnit -> !lectureUnitRepository.findWithEagerCompletedUsersByIdElseThrow(lectureUnit.getId()).isCompletedFor(learningPath.getUser())), + competency.getExercises().stream() + .filter(exercise -> !exerciseRepository.findByIdWithStudentParticipationsElseThrow(exercise.getId()).isCompletedFor(learningPath.getUser())))) + .findFirst().orElseThrow(); + } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java index 44f068602317..b7b2a54bd075 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java @@ -8,15 +8,18 @@ import org.springframework.web.bind.annotation.*; import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.LearningObject; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.competency.LearningPath; import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.security.Role; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor; +import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent; import de.tum.in.www1.artemis.service.AuthorizationCheckService; import de.tum.in.www1.artemis.service.LearningPathService; import de.tum.in.www1.artemis.web.rest.dto.PageableSearchDTO; import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; +import de.tum.in.www1.artemis.web.rest.dto.learningpath.NgxLearningPathDTO; @RestController @RequestMapping("/api") @@ -32,12 +35,15 @@ public class LearningPathResource { private final LearningPathService learningPathService; + private final LearningPathRepository learningPathRepository; + public LearningPathResource(CourseRepository courseRepository, AuthorizationCheckService authorizationCheckService, UserRepository userRepository, - LearningPathService learningPathService) { + LearningPathService learningPathService, LearningPathRepository learningPathRepository) { this.courseRepository = courseRepository; this.authorizationCheckService = authorizationCheckService; this.userRepository = userRepository; this.learningPathService = learningPathService; + this.learningPathRepository = learningPathRepository; } /** @@ -85,4 +91,39 @@ public ResponseEntity> getLearningPathsOnPage( return ResponseEntity.ok(learningPathService.getAllOfCourseOnPageWithSize(search, course)); } + + /** + * GET /courses/:courseId/learning-path-graph : Gets the ngx representation of the learning path. + * + * @param courseId the id of the course from which the learning path should be fetched + * @return the ResponseEntity with status 200 (OK) and with body the ngx representation of the learning path + */ + @GetMapping("/courses/{courseId}/learning-path-graph") + @EnforceAtLeastStudent + public ResponseEntity getNgxLearningPath(@PathVariable Long courseId) { + log.debug("REST request to get ngx representation of learning path for course with id: {}", courseId); + Course course = courseRepository.findByIdElseThrow(courseId); + User user = userRepository.getUserWithGroupsAndAuthorities(); + authorizationCheckService.isStudentInCourse(course, user); + if (!course.getLearningPathsEnabled()) { + throw new BadRequestException("Learning paths are not enabled for this course."); + } + LearningPath learningPath = learningPathRepository.findWithEagerCompetenciesAndLearningUnitsByCourseIdAndUserIdElseThrow(course.getId(), user.getId()); + NgxLearningPathDTO graph = learningPathService.generateNgxRepresentation(learningPath); + return ResponseEntity.ok(graph); + } + + /** + * GET /learning-path/:learningPathId/recommendation : Gets the next recommended learning object for the learning path. + * + * @param learningPathId the id of the learning path from which the recommendation should be fetched + * @return the ResponseEntity with status 200 (OK) and with body the recommended learning object + */ + @GetMapping("/learning-path/{learningPathId}/recommendation") + @EnforceAtLeastStudent + public ResponseEntity getRecommendation(@PathVariable Long learningPathId) { + log.debug("REST request to get recommendation for learning path with id: {}", learningPathId); + LearningPath learningPath = learningPathRepository.findWithEagerCompetenciesAndLearningUnitsByIdElseThrow(learningPathId); + return ResponseEntity.ok(learningPathService.getRecommendation(learningPath)); + } } diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.html b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.html new file mode 100644 index 000000000000..ca0843db9214 --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.html @@ -0,0 +1,4 @@ +
+ + +
diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.ts new file mode 100644 index 000000000000..23d8ca4149b5 --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.ts @@ -0,0 +1,24 @@ +import { Component, Input } from '@angular/core'; +import { faCheckCircle, faCircle, faInfoCircle } from '@fortawesome/free-solid-svg-icons'; + +export enum NodeType { + COMPETENCY_START, + COMPETENCY_END, + MATCH_START, + MATCH_END, + COMPLETED, +} +@Component({ + selector: 'jhi-learning-path-graph-node', + templateUrl: './learning-path-graph-node.component.html', +}) +export class LearningPathGraphNodeComponent { + @Input() + type: NodeType; + + //icons + faCheckCircle = faCheckCircle; + faInfoCircle = faInfoCircle; + faCircle = faCircle; + protected readonly NodeType = NodeType; +} diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.html b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.html new file mode 100644 index 000000000000..7da8df136dd7 --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.html @@ -0,0 +1,46 @@ +
+ + + + + + + + + + + + + {{ node.label }} asdff + + + + + + + asdf + + + + + + + + + +
diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.scss b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.scss new file mode 100644 index 000000000000..a995d3a4614a --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.scss @@ -0,0 +1,10 @@ +.graph-container { + display: block; + width: 100%; + height: 100%; + overflow: hidden; + + .ngx-graph { + width: auto; + } +} diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts new file mode 100644 index 000000000000..7e51bf7ebf10 --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts @@ -0,0 +1,56 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Layout } from '@swimlane/ngx-graph'; +import * as shape from 'd3-shape'; +import { Subject } from 'rxjs'; +import { LearningPathService } from 'app/course/learning-paths/learning-path.service'; +import { NgxLearningPathDTO } from 'app/entities/learning-path.model'; + +@Component({ + selector: 'jhi-learning-path-graph', + styleUrls: ['./learning-path-graph.component.scss'], + templateUrl: './learning-path-graph.component.html', +}) +export class LearningPathGraphComponent implements OnInit { + isLoading = false; + courseId: number; + ngxLearningPath: NgxLearningPathDTO; + + layout: string | Layout = 'dagreCluster'; + curve = shape.curveBundle; + + draggingEnabled = false; + panningEnabled = true; + zoomEnabled = true; + panOnZoom = true; + + update$: Subject = new Subject(); + center$: Subject = new Subject(); + zoomToFit$: Subject = new Subject(); + + constructor(private activatedRoute: ActivatedRoute, private learningPathService: LearningPathService) {} + + ngOnInit() { + this.activatedRoute.parent!.parent!.params.subscribe((params) => { + this.courseId = +params['courseId']; + if (this.courseId) { + this.loadData(); + } + }); + } + + loadData() { + this.isLoading = true; + this.learningPathService.getNgxLearningPath(this.courseId).subscribe((ngxLearningPathResponse) => { + this.ngxLearningPath = ngxLearningPathResponse.body!; + console.log(this.ngxLearningPath); + this.isLoading = false; + }); + } + + onResize() { + this.update$.next(true); + this.center$.next(true); + this.zoomToFit$.next(true); + } +} diff --git a/src/main/webapp/app/course/learning-paths/learning-path.service.ts b/src/main/webapp/app/course/learning-paths/learning-path.service.ts index 5cf49c2c8115..5d9478413077 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path.service.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path.service.ts @@ -2,6 +2,8 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { HttpClient, HttpResponse } from '@angular/common/http'; import { Course } from 'app/entities/course.model'; +import { NgxLearningPathDTO } from 'app/entities/learning-path.model'; +import { map } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class LearningPathService { @@ -12,4 +14,21 @@ export class LearningPathService { enableLearningPaths(courseId: number): Observable> { return this.httpClient.put(`${this.resourceURL}/courses/${courseId}/learning-paths/enable`, null, { observe: 'response' }); } + + getNgxLearningPath(courseId: number): Observable> { + return this.httpClient.get(`${this.resourceURL}/courses/${courseId}/learning-path-graph`, { observe: 'response' }).pipe( + map((ngxLearningPathResponse) => { + if (!ngxLearningPathResponse.body!.nodes) { + ngxLearningPathResponse.body!.nodes = []; + } + if (!ngxLearningPathResponse.body!.edges) { + ngxLearningPathResponse.body!.edges = []; + } + if (!ngxLearningPathResponse.body!.clusters) { + ngxLearningPathResponse.body!.clusters = []; + } + return ngxLearningPathResponse; + }), + ); + } } diff --git a/src/main/webapp/app/course/learning-paths/learning-paths.module.ts b/src/main/webapp/app/course/learning-paths/learning-paths.module.ts index 690dcc42db1e..124e9b21c81d 100644 --- a/src/main/webapp/app/course/learning-paths/learning-paths.module.ts +++ b/src/main/webapp/app/course/learning-paths/learning-paths.module.ts @@ -1,14 +1,46 @@ import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; +import { RouterModule, Routes } from '@angular/router'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { NgxGraphModule } from '@swimlane/ngx-graph'; import { LearningPathManagementComponent } from 'app/course/learning-paths/learning-path-management/learning-path-management.component'; +import { LearningPathContainerComponent } from 'app/course/learning-paths/participate/learning-path-container.component'; +import { Authority } from 'app/shared/constants/authority.constants'; +import { UserRouteAccessService } from 'app/core/auth/user-route-access-service'; +import { LearningPathGraphSidebarComponent } from 'app/course/learning-paths/participate/learning-path-graph-sidebar.component'; +import { LearningPathGraphComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph.component'; +import { LearningPathGraphNodeComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph-node.component'; +import { NgxGraphModule } from '@swimlane/ngx-graph'; +import { ArtemisLearningPathLectureUnitViewModule } from 'app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.module'; +const routes: Routes = [ + { + path: '', + pathMatch: 'prefix', + data: { + authorities: [Authority.USER], + pageTitle: 'overview.learningPath', + }, + canActivate: [UserRouteAccessService], + children: [ + { + path: '', + pathMatch: 'full', + component: LearningPathContainerComponent, + children: [ + { + path: '', + pathMatch: 'full', + loadChildren: () => import('app/overview/discussion-section/discussion-section.module').then((m) => m.DiscussionSectionModule), + }, + ], + }, + ], + }, +]; @NgModule({ - imports: [ArtemisSharedModule, FormsModule, ReactiveFormsModule, NgxGraphModule, ArtemisSharedComponentModule, RouterModule], - declarations: [LearningPathManagementComponent], - exports: [], + imports: [ArtemisSharedModule, FormsModule, ReactiveFormsModule, ArtemisSharedComponentModule, RouterModule, NgxGraphModule, ArtemisLearningPathLectureUnitViewModule], + declarations: [LearningPathContainerComponent, LearningPathManagementComponent, LearningPathGraphSidebarComponent, LearningPathGraphComponent, LearningPathGraphNodeComponent], + exports: [LearningPathContainerComponent], }) export class ArtemisLearningPathsModule {} diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html new file mode 100644 index 000000000000..689e411713ea --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html @@ -0,0 +1,18 @@ +
+
+ +
+
+ content more content far more content + + + +
+
discussions discussions discussions
+
+ + diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts new file mode 100644 index 000000000000..faa0184d4efd --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts @@ -0,0 +1,65 @@ +import { Component, Input } from '@angular/core'; +import { faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons'; +import { Exercise } from 'app/entities/exercise.model'; +import { LectureUnit } from 'app/entities/lecture-unit/lectureUnit.model'; +import { Lecture } from 'app/entities/lecture.model'; + +@Component({ + selector: 'jhi-learning-path-container', + templateUrl: './learning-path-container.component.html', +}) +export class LearningPathContainerComponent { + @Input() + courseId: number; + + lecture: Lecture | undefined; + lectureUnit: LectureUnit | undefined; + exercise: Exercise | undefined; + history: any[] = []; + + // icons + faChevronLeft = faChevronLeft; + faChevronRight = faChevronRight; + + constructor() {} + + onNextTask() { + if (this.lectureUnit?.id) { + this.history.push(this.lectureUnit.id); + this.lectureUnit = undefined; + } else if (this.exercise?.id) { + this.history.push(this.exercise.id); + this.lectureUnit = undefined; + } + console.log('request next task'); + } + + undefineAll() { + this.lecture = undefined; + this.lectureUnit = undefined; + this.exercise = undefined; + } + + onPrevTask() { + this.undefineAll(); + const task = this.history.pop(); + if (!task) { + return; + } else if (task instanceof LectureUnit) { + this.lectureUnit = task; + this.loadLectureUnit(); + } else { + this.exercise = task; + this.loadExercise(); + } + console.log('request previous task'); + } + + loadLectureUnit() { + console.log('load lecture unit'); + } + + loadExercise() { + console.log('load exercise'); + } +} diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.html b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.html new file mode 100644 index 000000000000..1cadb8f0904d --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.html @@ -0,0 +1,31 @@ +
+
+
+ +
+

+ + {{ 'artemisApp.learningPath.sideBar.header' | artemisTranslate }} +

+
+ +
+
+ +
+ + +
+
+
+ +
+
+ +
+ + Learning Path + + +
+
diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.scss b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.scss new file mode 100644 index 000000000000..9b4ce9f778d7 --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.scss @@ -0,0 +1,86 @@ +@import 'src/main/webapp/content/scss/artemis-variables'; + +$draggable-width: 15px; +$graph-min-width: 215px; + +.learning-path-sidebar { + .expanded-graph { + display: flex; + width: calc(#{$draggable-width} + #{$graph-min-width}); + margin-left: auto; + + .scrollbar { + position: relative; + max-height: 700px; + overflow: auto; + } + + .wrapper-scroll-y { + display: block; + } + + .draggable-right { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-width: $draggable-width; + } + + .card { + width: inherit; + min-width: $graph-min-width; + + .card-header { + display: inline-flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + + .card-title { + display: flex; + } + + .row > .col-auto:last-child { + display: flex; + flex-direction: column; + justify-content: center; + } + } + + .card-body { + padding: 0; + } + } + } + + .collapsed-graph { + display: flex; + width: 38px; + justify-content: space-between; + flex-flow: column; + cursor: pointer; + + span { + writing-mode: vertical-lr; + transform: rotate(180deg); + margin: auto; + } + + .expand-graph-icon { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + place-self: center; + } + } + + @media screen and (max-width: 992px) { + .expanded-graph { + width: 94vw; + + .draggable-right { + display: none; + } + } + } +} diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.ts b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.ts new file mode 100644 index 000000000000..ab4e0de7ef6b --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.ts @@ -0,0 +1,53 @@ +import { AfterViewInit, Component, ViewChild } from '@angular/core'; +import interact from 'interactjs'; +import { faChevronLeft, faChevronRight, faGripLinesVertical, faNetworkWired } from '@fortawesome/free-solid-svg-icons'; +import { Course } from 'app/entities/course.model'; +import { LearningPathGraphComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph.component'; + +@Component({ + selector: 'jhi-learning-path-sidebar', + styleUrls: ['./learning-path-graph-sidebar.component.scss'], + templateUrl: './learning-path-graph-sidebar.component.html', +}) +export class LearningPathGraphSidebarComponent implements AfterViewInit { + course?: Course; + + collapsed: boolean; + // Icons + faChevronLeft = faChevronLeft; + faChevronRight = faChevronRight; + faGripLinesVertical = faGripLinesVertical; + faNetworkWired = faNetworkWired; + + @ViewChild(`learningPathGraphComponent`, { static: false }) + learningPathGraphComponent: LearningPathGraphComponent; + + constructor() {} + + ngAfterViewInit(): void { + // allows the conversation sidebar to be resized towards the right-hand side + interact('.expanded-graph') + .resizable({ + edges: { left: false, right: '.draggable-right', bottom: false, top: false }, + modifiers: [ + // Set maximum width of the conversation sidebar + interact.modifiers!.restrictSize({ + min: { width: 230, height: 0 }, + max: { width: 500, height: 4000 }, + }), + ], + inertia: true, + }) + .on('resizestart', (event: any) => { + event.target.classList.add('card-resizable'); + }) + .on('resizeend', (event: any) => { + event.target.classList.remove('card-resizable'); + this.learningPathGraphComponent.onResize(); + }) + .on('resizemove', (event: any) => { + const target = event.target; + target.style.width = event.rect.width + 'px'; + }); + } +} diff --git a/src/main/webapp/app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.component.html b/src/main/webapp/app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.component.html new file mode 100644 index 000000000000..c6f4bcd378d1 --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.component.html @@ -0,0 +1,6 @@ +
+ + + + +
diff --git a/src/main/webapp/app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.component.ts b/src/main/webapp/app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.component.ts new file mode 100644 index 000000000000..2bb1981d3dcf --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.component.ts @@ -0,0 +1,38 @@ +import { Component, Input } from '@angular/core'; +import { HttpErrorResponse } from '@angular/common/http'; +import { LectureUnit, LectureUnitType } from 'app/entities/lecture-unit/lectureUnit.model'; +import { onError } from 'app/shared/util/global.utils'; +import { Lecture } from 'app/entities/lecture.model'; +import { LectureUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/lectureUnit.service'; +import { AlertService } from 'app/core/util/alert.service'; + +export interface LectureUnitCompletionEvent { + lectureUnit: LectureUnit; + completed: boolean; +} + +@Component({ + selector: 'jhi-learning-path-lecture-unit-view', + templateUrl: './learning-path-lecture-unit-view.component.html', +}) +export class LearningPathLectureUnitViewComponent { + @Input() + lecture: Lecture; + @Input() + lectureUnit: LectureUnit; + + readonly LectureUnitType = LectureUnitType; + + constructor(private lectureUnitService: LectureUnitService, private alertService: AlertService) {} + + completeLectureUnit(event: LectureUnitCompletionEvent): void { + if (this.lecture && event.lectureUnit.visibleToStudents && event.lectureUnit.completed !== event.completed) { + this.lectureUnitService.setCompletion(event.lectureUnit.id!, this.lecture.id!, event.completed).subscribe({ + next: () => { + event.lectureUnit.completed = event.completed; + }, + error: (res: HttpErrorResponse) => onError(this.alertService, res), + }); + } + } +} diff --git a/src/main/webapp/app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.module.ts b/src/main/webapp/app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.module.ts new file mode 100644 index 000000000000..2e1f5bfc6c7d --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { LearningPathLectureUnitViewComponent } from 'app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.component'; +import { ArtemisLectureUnitsModule } from 'app/overview/course-lectures/lecture-units.module'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; + +@NgModule({ + imports: [ArtemisSharedModule, ArtemisLectureUnitsModule], + declarations: [LearningPathLectureUnitViewComponent], + exports: [LearningPathLectureUnitViewComponent], +}) +export class ArtemisLearningPathLectureUnitViewModule {} diff --git a/src/main/webapp/app/entities/learning-path.model.ts b/src/main/webapp/app/entities/learning-path.model.ts index b36aac4a5f73..025a723c292d 100644 --- a/src/main/webapp/app/entities/learning-path.model.ts +++ b/src/main/webapp/app/entities/learning-path.model.ts @@ -2,6 +2,7 @@ import { BaseEntity } from 'app/shared/model/base-entity'; import { Course } from 'app/entities/course.model'; import { User } from 'app/core/user/user.model'; import { Competency } from 'app/entities/competency.model'; +import { ClusterNode, Edge, Node } from '@swimlane/ngx-graph'; export class LearningPath implements BaseEntity { public id?: number; @@ -12,3 +13,36 @@ export class LearningPath implements BaseEntity { constructor() {} } + +export class NgxLearningPathDTO { + public nodes: NgxLearningPathNode[]; + public edges: NgxLearningPathEdge[]; + public clusters: NgxLearningPathCluster[]; +} + +export class NgxLearningPathNode implements Node { + public id: string; + public type?: NodeType; + public linkedResource?: number; + public label?: string; +} + +export class NgxLearningPathEdge implements Edge { + public id?: string; + public source: string; + public target: string; +} + +export class NgxLearningPathCluster implements ClusterNode { + public id: string; + public label?: string; + public childNodeIds?: string[]; +} +export enum NodeType { + COMPETENCY_START, + COMPETENCY_END, + MATCH_START, + MATCH_END, + EXERCISE, + LECTURE_UNIT, +} diff --git a/src/main/webapp/app/overview/course-learning-path/course-learning-path.component.html b/src/main/webapp/app/overview/course-learning-path/course-learning-path.component.html new file mode 100644 index 000000000000..4cd61a1c7b63 --- /dev/null +++ b/src/main/webapp/app/overview/course-learning-path/course-learning-path.component.html @@ -0,0 +1 @@ + diff --git a/src/main/webapp/app/overview/course-learning-path/course-learning-path.component.ts b/src/main/webapp/app/overview/course-learning-path/course-learning-path.component.ts new file mode 100644 index 000000000000..b6b3e0cb779d --- /dev/null +++ b/src/main/webapp/app/overview/course-learning-path/course-learning-path.component.ts @@ -0,0 +1,20 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +@Component({ + selector: 'jhi-course-learning-path', + templateUrl: './course-learning-path.component.html', + styleUrls: ['../course-overview.scss'], +}) +export class CourseLearningPathComponent implements OnInit { + @Input() + courseId: number; + + constructor(private activatedRoute: ActivatedRoute) {} + + ngOnInit(): void { + this.activatedRoute.parent?.parent?.params.subscribe((params) => { + this.courseId = parseInt(params['courseId'], 10); + }); + } +} diff --git a/src/main/webapp/app/overview/course-learning-path/course-learning-path.module.ts b/src/main/webapp/app/overview/course-learning-path/course-learning-path.module.ts new file mode 100644 index 000000000000..538538611bcf --- /dev/null +++ b/src/main/webapp/app/overview/course-learning-path/course-learning-path.module.ts @@ -0,0 +1,28 @@ +import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; +import { Authority } from 'app/shared/constants/authority.constants'; +import { UserRouteAccessService } from 'app/core/auth/user-route-access-service'; +import { ArtemisLearningPathsModule } from 'app/course/learning-paths/learning-paths.module'; +import { CourseLearningPathComponent } from 'app/overview/course-learning-path/course-learning-path.component'; + +const routes: Routes = [ + { + path: '', + pathMatch: 'full', + data: { + authorities: [Authority.USER], + pageTitle: 'overview.learningPath', + }, + component: CourseLearningPathComponent, + canActivate: [UserRouteAccessService], + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes), ArtemisSharedModule, ArtemisSharedComponentModule, ArtemisLearningPathsModule], + declarations: [CourseLearningPathComponent], + exports: [CourseLearningPathComponent], +}) +export class CourseLearningPathModule {} diff --git a/src/main/webapp/app/overview/course-overview.component.html b/src/main/webapp/app/overview/course-overview.component.html index 155f6519105c..0a593e403404 100644 --- a/src/main/webapp/app/overview/course-overview.component.html +++ b/src/main/webapp/app/overview/course-overview.component.html @@ -24,6 +24,16 @@
Competencies
+
+
Learning Path
+
Statistics
diff --git a/src/main/webapp/app/overview/courses-routing.module.ts b/src/main/webapp/app/overview/courses-routing.module.ts index 3272d689f660..8fed271b0a52 100644 --- a/src/main/webapp/app/overview/courses-routing.module.ts +++ b/src/main/webapp/app/overview/courses-routing.module.ts @@ -65,6 +65,10 @@ const routes: Routes = [ path: 'competencies', loadChildren: () => import('./course-competencies/course-competencies.module').then((m) => m.CourseCompetenciesModule), }, + { + path: 'learning-path', + loadChildren: () => import('./course-learning-path/course-learning-path.module').then((m) => m.CourseLearningPathModule), + }, { path: 'discussion', loadChildren: () => import('./course-discussion/course-discussion.module').then((m) => m.CourseDiscussionModule), diff --git a/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts b/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts index 9f10b6db9ddd..3512c76bbcde 100644 --- a/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts +++ b/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts @@ -269,6 +269,7 @@ export class NavbarComponent implements OnInit, OnDestroy { exercise_hints: 'artemisApp.exerciseHint.home.title', ratings: 'artemisApp.ratingList.pageTitle', competency_management: 'artemisApp.competency.manageCompetencies.title', + learning_path_management: 'artemisApp.learningPath.manageLearningPaths.title', assessment_locks: 'artemisApp.assessment.locks.home.title', apollon_diagrams: 'artemisApp.apollonDiagram.home.title', communication: 'artemisApp.metis.communication.label', @@ -338,6 +339,7 @@ export class NavbarComponent implements OnInit, OnDestroy { exercises: 'artemisApp.courseOverview.menu.exercises', lectures: 'artemisApp.courseOverview.menu.lectures', competencies: 'artemisApp.courseOverview.menu.competencies', + learning_path: 'artemisApp.courseOverview.menu.learningPath', statistics: 'artemisApp.courseOverview.menu.statistics', discussion: 'artemisApp.metis.communication.label', messages: 'artemisApp.conversationsLayout.breadCrumbLabel', diff --git a/src/main/webapp/i18n/de/competency.json b/src/main/webapp/i18n/de/competency.json index 63422348f032..bd9fa4e9c696 100644 --- a/src/main/webapp/i18n/de/competency.json +++ b/src/main/webapp/i18n/de/competency.json @@ -163,6 +163,11 @@ "progress": "Fortschritt", "view": "Ansehen" } + }, + "sideBar": { + "hide": "Lernpfad ausbleden", + "show": "Lernpfad einblenden", + "header": "Lernpfad" } } } diff --git a/src/main/webapp/i18n/de/global.json b/src/main/webapp/i18n/de/global.json index 47b23e6aceb7..736a4b419672 100644 --- a/src/main/webapp/i18n/de/global.json +++ b/src/main/webapp/i18n/de/global.json @@ -327,6 +327,7 @@ "exercise": "Aufgabendetails", "lectures": "Vorlesungen", "competencies": "Kompetenzen", + "learningPath": "Lernpfad", "tutorialGroups": "Übungsgruppen", "statistics": "Kursstatistiken", "exams": "Klausuren", diff --git a/src/main/webapp/i18n/de/student-dashboard.json b/src/main/webapp/i18n/de/student-dashboard.json index 1c480a3881bb..19d5f9c1a1e4 100644 --- a/src/main/webapp/i18n/de/student-dashboard.json +++ b/src/main/webapp/i18n/de/student-dashboard.json @@ -33,6 +33,7 @@ "statistics": "Statistiken", "lectures": "Vorlesungen", "competencies": "Kompetenzen", + "learningPath": "Lernpfad", "tutorialGroups": "Übungsgruppen", "exams": "Klausuren", "testExam": "Testklausur", diff --git a/src/main/webapp/i18n/en/competency.json b/src/main/webapp/i18n/en/competency.json index a731da8d8fcd..bd778500d71f 100644 --- a/src/main/webapp/i18n/en/competency.json +++ b/src/main/webapp/i18n/en/competency.json @@ -162,6 +162,11 @@ "progress": "Progress", "view": "View" } + }, + "sideBar": { + "hide": "Hide Learning Path", + "show": "Show Learning Path", + "header": "Learning Path" } } } diff --git a/src/main/webapp/i18n/en/global.json b/src/main/webapp/i18n/en/global.json index 8c3ea4a9a1fa..ff0bb670356c 100644 --- a/src/main/webapp/i18n/en/global.json +++ b/src/main/webapp/i18n/en/global.json @@ -327,6 +327,7 @@ "course": "Course details", "lectures": "Lectures", "competencies": "Competencies", + "learningPath": "Learning Path", "tutorialGroups": "Tutorial Groups", "exercise": "Exercise details", "statistics": "Course statistics", diff --git a/src/main/webapp/i18n/en/student-dashboard.json b/src/main/webapp/i18n/en/student-dashboard.json index 5ef55d54314c..f3edd5bbd579 100644 --- a/src/main/webapp/i18n/en/student-dashboard.json +++ b/src/main/webapp/i18n/en/student-dashboard.json @@ -34,6 +34,7 @@ "lectures": "Lectures", "tutorialGroups": "Tutorial Groups", "competencies": "Competencies", + "learningPath": "Learning Path", "exams": "Exams", "testExam": "Test Exam", "communication": "Communication", diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java index 675f5261d494..b0c0c95cadee 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java @@ -32,6 +32,7 @@ import de.tum.in.www1.artemis.service.LectureUnitService; import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.util.PageableSearchUtilService; +import de.tum.in.www1.artemis.web.rest.dto.learningpath.NgxLearningPathDTO; class LearningPathIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { @@ -349,4 +350,23 @@ void testUpdateLearningPathProgress() throws Exception { learningPath = learningPathRepository.findWithEagerCompetenciesByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); assertThat(learningPath.getProgress()).as("contains completed competency").isNotEqualTo(0); } + + @Test + @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") + void testGetNgxLearningPathForLearningPathsDisabled() throws Exception { + request.get("/api/courses/" + course.getId() + "/learning-path-graph", HttpStatus.BAD_REQUEST, NgxLearningPathDTO.class); + } + + /** + * This only tests if the end point successfully retrieves the graph representation. The correctness of the response is tested in LearningPathServiceTest. + * + * @throws Exception the request failed + * @see de.tum.in.www1.artemis.service.LearningPathServiceTest + */ + @Test + @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") + void testGetNgxLearningPath() throws Exception { + course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); + request.get("/api/courses/" + course.getId() + "/learning-path-graph", HttpStatus.OK, NgxLearningPathDTO.class); + } } From 13027e409ae053d78ec561b65234e3e0f22eefd0 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Fri, 14 Jul 2023 12:36:41 +0200 Subject: [PATCH 051/215] Fix server test --- .../tum/in/www1/artemis/service/LearningPathServiceTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java index 1451542984f1..41388e7844b6 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java @@ -30,7 +30,7 @@ import de.tum.in.www1.artemis.security.SecurityUtils; import de.tum.in.www1.artemis.web.rest.dto.learningpath.NgxLearningPathDTO; -public class LearningPathServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class LearningPathServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { @Autowired LearningPathService learningPathService; @@ -122,7 +122,7 @@ void testCompetencyWithLectureUnitAndExercise() { new NgxLearningPathDTO.Edge(LearningPathService.getLectureUnitOutEdgeId(competency.getId(), lectureUnit.getId()), LearningPathService.getLectureUnitNodeId(competency.getId(), lectureUnit.getId()), endNodeId), new NgxLearningPathDTO.Edge(LearningPathService.getExerciseInEdgeId(competency.getId(), exercise.getId()), startNodeId, - LearningPathService.getExerciseNodeId(competency.getId(), lectureUnit.getId())), + LearningPathService.getExerciseNodeId(competency.getId(), exercise.getId())), new NgxLearningPathDTO.Edge(LearningPathService.getExerciseOutEdgeId(competency.getId(), exercise.getId()), LearningPathService.getExerciseNodeId(competency.getId(), exercise.getId()), endNodeId)); Set expectedClusters = Set.of(new NgxLearningPathDTO.Cluster(String.valueOf(competency.getId()), competency.getTitle(), From 1c8a0254d954b9ffc345b957d9ad0e2ab5f77b18 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Sat, 15 Jul 2023 17:16:56 +0200 Subject: [PATCH 052/215] fix LP Lecture Unit view --- .../repository/LearningPathRepository.java | 4 + .../artemis/service/LearningPathService.java | 2 +- .../web/rest/LearningPathResource.java | 36 ++++++- .../LearningPathRecommendation.java | 8 ++ .../learning-path-graph.component.ts | 21 ++-- .../learning-paths/learning-path.service.ts | 10 +- .../learning-paths/learning-paths.module.ts | 24 +++-- .../learning-path-container.component.html | 28 +++-- .../learning-path-container.component.ts | 102 +++++++++++++++--- ...arning-path-lecture-unit-view.component.ts | 7 +- .../app/entities/learning-path.model.ts | 12 +++ .../course-learning-path.component.ts | 4 +- .../discussion-section.component.ts | 7 +- src/main/webapp/i18n/de/competency.json | 5 + src/main/webapp/i18n/en/competency.json | 5 + .../service/LearningPathServiceTest.java | 27 +++++ 16 files changed, 249 insertions(+), 53 deletions(-) create mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/LearningPathRecommendation.java diff --git a/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java index 54cbec29d067..295d2b5ba610 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java @@ -23,6 +23,10 @@ public interface LearningPathRepository extends JpaRepository findByCourseIdAndUserId(long courseId, long userId); + default LearningPath findByCourseIdAndUserIdElseThrow(long courseId, long userId) { + return findByCourseIdAndUserId(courseId, userId).orElseThrow(() -> new EntityNotFoundException("LearningPath")); + } + @EntityGraph(type = LOAD, attributePaths = { "competencies" }) Optional findWithEagerCompetenciesByCourseIdAndUserId(long courseId, long userId); diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index 3995bd50f897..fdeb726167c7 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -389,6 +389,6 @@ public LearningObject getRecommendation(LearningPath learningPath) { .filter(lectureUnit -> !lectureUnitRepository.findWithEagerCompletedUsersByIdElseThrow(lectureUnit.getId()).isCompletedFor(learningPath.getUser())), competency.getExercises().stream() .filter(exercise -> !exerciseRepository.findByIdWithStudentParticipationsElseThrow(exercise.getId()).isCompletedFor(learningPath.getUser())))) - .findFirst().orElseThrow(); + .findFirst().orElse(null); } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java index b7b2a54bd075..2f9b21d39290 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java @@ -11,6 +11,7 @@ import de.tum.in.www1.artemis.domain.LearningObject; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.competency.LearningPath; +import de.tum.in.www1.artemis.domain.lecture.LectureUnit; import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.security.Role; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor; @@ -19,6 +20,7 @@ import de.tum.in.www1.artemis.service.LearningPathService; import de.tum.in.www1.artemis.web.rest.dto.PageableSearchDTO; import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; +import de.tum.in.www1.artemis.web.rest.dto.learningpath.LearningPathRecommendation; import de.tum.in.www1.artemis.web.rest.dto.learningpath.NgxLearningPathDTO; @RestController @@ -113,6 +115,26 @@ public ResponseEntity getNgxLearningPath(@PathVariable Long return ResponseEntity.ok(graph); } + /** + * GET /courses/:courseId/learning-path-id : Gets the id of the learning path. + * + * @param courseId the id of the course from which the learning path id should be fetched + * @return the ResponseEntity with status 200 (OK) and with body the id of the learning path + */ + @GetMapping("/courses/{courseId}/learning-path-id") + @EnforceAtLeastStudent + public ResponseEntity getLearningPathId(@PathVariable Long courseId) { + log.debug("REST request to get learning path id for course with id: {}", courseId); + Course course = courseRepository.findByIdElseThrow(courseId); + User user = userRepository.getUserWithGroupsAndAuthorities(); + authorizationCheckService.isStudentInCourse(course, user); + if (!course.getLearningPathsEnabled()) { + throw new BadRequestException("Learning paths are not enabled for this course."); + } + LearningPath learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), user.getId()); + return ResponseEntity.ok(learningPath.getId()); + } + /** * GET /learning-path/:learningPathId/recommendation : Gets the next recommended learning object for the learning path. * @@ -121,9 +143,19 @@ public ResponseEntity getNgxLearningPath(@PathVariable Long */ @GetMapping("/learning-path/{learningPathId}/recommendation") @EnforceAtLeastStudent - public ResponseEntity getRecommendation(@PathVariable Long learningPathId) { + public ResponseEntity getRecommendation(@PathVariable Long learningPathId) { log.debug("REST request to get recommendation for learning path with id: {}", learningPathId); LearningPath learningPath = learningPathRepository.findWithEagerCompetenciesAndLearningUnitsByIdElseThrow(learningPathId); - return ResponseEntity.ok(learningPathService.getRecommendation(learningPath)); + LearningObject recommendation = learningPathService.getRecommendation(learningPath); + if (recommendation == null) { + return ResponseEntity.ok(new LearningPathRecommendation(-1, -1, LearningPathRecommendation.RecommendationType.EMPTY)); + } + else if (recommendation instanceof LectureUnit lectureUnit) { + return ResponseEntity + .ok(new LearningPathRecommendation(recommendation.getId(), lectureUnit.getLecture().getId(), LearningPathRecommendation.RecommendationType.LECTURE_UNIT)); + } + else { + return ResponseEntity.ok(new LearningPathRecommendation(recommendation.getId(), -1, LearningPathRecommendation.RecommendationType.EXERCISE)); + } } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/LearningPathRecommendation.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/LearningPathRecommendation.java new file mode 100644 index 000000000000..a43920d0262c --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/LearningPathRecommendation.java @@ -0,0 +1,8 @@ +package de.tum.in.www1.artemis.web.rest.dto.learningpath; + +public record LearningPathRecommendation(long learningObjectId, long lectureId, RecommendationType type) { + + public enum RecommendationType { + EMPTY, LECTURE_UNIT, EXERCISE + } +} diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts index 7e51bf7ebf10..9731392cff7b 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Layout } from '@swimlane/ngx-graph'; import * as shape from 'd3-shape'; @@ -13,7 +13,7 @@ import { NgxLearningPathDTO } from 'app/entities/learning-path.model'; }) export class LearningPathGraphComponent implements OnInit { isLoading = false; - courseId: number; + @Input() courseId: number; ngxLearningPath: NgxLearningPathDTO; layout: string | Layout = 'dagreCluster'; @@ -31,19 +31,22 @@ export class LearningPathGraphComponent implements OnInit { constructor(private activatedRoute: ActivatedRoute, private learningPathService: LearningPathService) {} ngOnInit() { - this.activatedRoute.parent!.parent!.params.subscribe((params) => { - this.courseId = +params['courseId']; - if (this.courseId) { - this.loadData(); - } - }); + if (!this.courseId) { + this.activatedRoute.parent!.parent!.params.subscribe((params) => { + this.courseId = +params['courseId']; + if (this.courseId) { + this.loadData(); + } + }); + } else { + this.loadData(); + } } loadData() { this.isLoading = true; this.learningPathService.getNgxLearningPath(this.courseId).subscribe((ngxLearningPathResponse) => { this.ngxLearningPath = ngxLearningPathResponse.body!; - console.log(this.ngxLearningPath); this.isLoading = false; }); } diff --git a/src/main/webapp/app/course/learning-paths/learning-path.service.ts b/src/main/webapp/app/course/learning-paths/learning-path.service.ts index 5d9478413077..ea5c3a3762c8 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path.service.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { HttpClient, HttpResponse } from '@angular/common/http'; import { Course } from 'app/entities/course.model'; -import { NgxLearningPathDTO } from 'app/entities/learning-path.model'; +import { LearningPathRecommendation, NgxLearningPathDTO, RecommendationType } from 'app/entities/learning-path.model'; import { map } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) @@ -31,4 +31,12 @@ export class LearningPathService { }), ); } + + getLearningPathId(courseId: number) { + return this.httpClient.get(`${this.resourceURL}/courses/${courseId}/learning-path-id`, { observe: 'response' }); + } + + getRecommendation(learningPathId: number) { + return this.httpClient.get(`${this.resourceURL}/learning-path/${learningPathId}/recommendation`, { observe: 'response' }); + } } diff --git a/src/main/webapp/app/course/learning-paths/learning-paths.module.ts b/src/main/webapp/app/course/learning-paths/learning-paths.module.ts index 124e9b21c81d..5d0740f39807 100644 --- a/src/main/webapp/app/course/learning-paths/learning-paths.module.ts +++ b/src/main/webapp/app/course/learning-paths/learning-paths.module.ts @@ -12,11 +12,12 @@ import { LearningPathGraphComponent } from 'app/course/learning-paths/learning-p import { LearningPathGraphNodeComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph-node.component'; import { NgxGraphModule } from '@swimlane/ngx-graph'; import { ArtemisLearningPathLectureUnitViewModule } from 'app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.module'; +import { ArtemisLectureUnitsModule } from 'app/overview/course-lectures/lecture-units.module'; const routes: Routes = [ { path: '', - pathMatch: 'prefix', + component: LearningPathContainerComponent, data: { authorities: [Authority.USER], pageTitle: 'overview.learningPath', @@ -26,20 +27,23 @@ const routes: Routes = [ { path: '', pathMatch: 'full', - component: LearningPathContainerComponent, - children: [ - { - path: '', - pathMatch: 'full', - loadChildren: () => import('app/overview/discussion-section/discussion-section.module').then((m) => m.DiscussionSectionModule), - }, - ], + loadChildren: () => import('app/overview/discussion-section/discussion-section.module').then((m) => m.DiscussionSectionModule), }, ], }, ]; + @NgModule({ - imports: [ArtemisSharedModule, FormsModule, ReactiveFormsModule, ArtemisSharedComponentModule, RouterModule, NgxGraphModule, ArtemisLearningPathLectureUnitViewModule], + imports: [ + ArtemisSharedModule, + FormsModule, + ReactiveFormsModule, + ArtemisSharedComponentModule, + NgxGraphModule, + ArtemisLearningPathLectureUnitViewModule, + RouterModule.forChild(routes), + ArtemisLectureUnitsModule, + ], declarations: [LearningPathContainerComponent, LearningPathManagementComponent, LearningPathGraphSidebarComponent, LearningPathGraphComponent, LearningPathGraphNodeComponent], exports: [LearningPathContainerComponent], }) diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html index 689e411713ea..1e146fa566c3 100644 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html @@ -3,16 +3,30 @@
- content more content far more content - - - +
+
+ + +
+ No task selected +
+
+
+ +
+
-
discussions discussions discussions
diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts index faa0184d4efd..ee3f4b6d5e5d 100644 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts @@ -1,37 +1,79 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; import { faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons'; import { Exercise } from 'app/entities/exercise.model'; -import { LectureUnit } from 'app/entities/lecture-unit/lectureUnit.model'; +import { LectureUnit, LectureUnitType } from 'app/entities/lecture-unit/lectureUnit.model'; import { Lecture } from 'app/entities/lecture.model'; +import { LearningPathService } from 'app/course/learning-paths/learning-path.service'; +import { RecommendationType } from 'app/entities/learning-path.model'; +import { LectureService } from 'app/lecture/lecture.service'; +import { onError } from 'app/shared/util/global.utils'; +import { HttpErrorResponse } from '@angular/common/http'; +import { AlertService } from 'app/core/util/alert.service'; +import { isCommunicationEnabled, isMessagingEnabled } from 'app/entities/course.model'; +import { DiscussionSectionComponent } from 'app/overview/discussion-section/discussion-section.component'; @Component({ selector: 'jhi-learning-path-container', templateUrl: './learning-path-container.component.html', }) -export class LearningPathContainerComponent { +export class LearningPathContainerComponent implements OnInit { @Input() courseId: number; + learningPathId: number; + learningObjectId: number; + lectureId?: number; lecture: Lecture | undefined; lectureUnit: LectureUnit | undefined; exercise: Exercise | undefined; - history: any[] = []; + history: [number, number][] = []; // icons faChevronLeft = faChevronLeft; faChevronRight = faChevronRight; - constructor() {} + discussionComponent?: DiscussionSectionComponent; + + constructor( + private router: Router, + private activatedRoute: ActivatedRoute, + private learningPathService: LearningPathService, + private lectureService: LectureService, + private alertService: AlertService, + ) {} + + ngOnInit() { + console.log('ON INIT CONTAINER'); + if (!this.courseId) { + this.activatedRoute.parent!.parent!.params.subscribe((params) => { + this.courseId = +params['courseId']; + }); + } + this.learningPathService.getLearningPathId(this.courseId).subscribe((learningPathIdResponse) => { + this.learningPathId = learningPathIdResponse.body!; + }); + } onNextTask() { if (this.lectureUnit?.id) { - this.history.push(this.lectureUnit.id); - this.lectureUnit = undefined; + this.history.push([this.lectureUnit.id, this.lectureId!]); } else if (this.exercise?.id) { - this.history.push(this.exercise.id); - this.lectureUnit = undefined; + this.history.push([this.exercise.id, -1]); } + this.undefineAll(); console.log('request next task'); + console.log(this.courseId); + this.learningPathService.getRecommendation(this.learningPathId).subscribe((recommendationResponse) => { + const recommendation = recommendationResponse.body!; + this.learningObjectId = recommendation.learningObjectId; + this.lectureId = recommendation.lectureId; + if (recommendation.type == RecommendationType.LECTURE_UNIT) { + this.loadLectureUnit(); + } else if (recommendation.type === RecommendationType.EXERCISE) { + this.loadExercise(); + } + }); } undefineAll() { @@ -45,21 +87,51 @@ export class LearningPathContainerComponent { const task = this.history.pop(); if (!task) { return; - } else if (task instanceof LectureUnit) { - this.lectureUnit = task; - this.loadLectureUnit(); } else { - this.exercise = task; - this.loadExercise(); + this.learningObjectId = task[0]; + this.lectureId = task[1]; + if (task[1] == -1) { + this.loadExercise(); + } else { + this.loadLectureUnit(); + } } console.log('request previous task'); } loadLectureUnit() { - console.log('load lecture unit'); + console.log('loading lecture unit'); + this.lectureService.findWithDetails(this.lectureId!).subscribe({ + next: (findLectureResult) => { + this.lecture = findLectureResult.body!; + if (this.lecture?.lectureUnits) { + this.lectureUnit = this.lecture.lectureUnits.find((lectureUnit) => lectureUnit.id === this.learningObjectId); + } + }, + error: (errorResponse: HttpErrorResponse) => onError(this.alertService, errorResponse), + }); } loadExercise() { console.log('load exercise'); } + + /** + * This function gets called if the router outlet gets activated. This is + * used only for the DiscussionComponent + * @param instance The component instance + */ + onChildActivate(instance: DiscussionSectionComponent) { + console.log(instance); + this.discussionComponent = instance; // save the reference to the component instance + if (this.lecture) { + instance.lecture = this.lecture; + instance.isCommunicationPage = false; + } + // todo exercise + } + + protected readonly isMessagingEnabled = isMessagingEnabled; + protected readonly isCommunicationEnabled = isCommunicationEnabled; + protected readonly LectureUnitType = LectureUnitType; } diff --git a/src/main/webapp/app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.component.ts b/src/main/webapp/app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.component.ts index 2bb1981d3dcf..af97c18bd9f7 100644 --- a/src/main/webapp/app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.component.ts +++ b/src/main/webapp/app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.component.ts @@ -16,11 +16,8 @@ export interface LectureUnitCompletionEvent { templateUrl: './learning-path-lecture-unit-view.component.html', }) export class LearningPathLectureUnitViewComponent { - @Input() - lecture: Lecture; - @Input() - lectureUnit: LectureUnit; - + @Input() lecture: Lecture; + @Input() lectureUnit: LectureUnit; readonly LectureUnitType = LectureUnitType; constructor(private lectureUnitService: LectureUnitService, private alertService: AlertService) {} diff --git a/src/main/webapp/app/entities/learning-path.model.ts b/src/main/webapp/app/entities/learning-path.model.ts index 025a723c292d..dc2390022442 100644 --- a/src/main/webapp/app/entities/learning-path.model.ts +++ b/src/main/webapp/app/entities/learning-path.model.ts @@ -46,3 +46,15 @@ export enum NodeType { EXERCISE, LECTURE_UNIT, } + +export class LearningPathRecommendation { + public learningObjectId: number; + public lectureId?: number; + public type: RecommendationType; +} + +export enum RecommendationType { + EMPTY = 'EMPTY', + LECTURE_UNIT = 'LECTURE_UNIT', + EXERCISE = 'EXERCISE', +} diff --git a/src/main/webapp/app/overview/course-learning-path/course-learning-path.component.ts b/src/main/webapp/app/overview/course-learning-path/course-learning-path.component.ts index b6b3e0cb779d..c9d342f129c1 100644 --- a/src/main/webapp/app/overview/course-learning-path/course-learning-path.component.ts +++ b/src/main/webapp/app/overview/course-learning-path/course-learning-path.component.ts @@ -13,8 +13,8 @@ export class CourseLearningPathComponent implements OnInit { constructor(private activatedRoute: ActivatedRoute) {} ngOnInit(): void { - this.activatedRoute.parent?.parent?.params.subscribe((params) => { - this.courseId = parseInt(params['courseId'], 10); + this.activatedRoute.parent!.parent!.params.subscribe((params) => { + this.courseId = +params['courseId']; }); } } diff --git a/src/main/webapp/app/overview/discussion-section/discussion-section.component.ts b/src/main/webapp/app/overview/discussion-section/discussion-section.component.ts index c4cc4e431a3f..9c544455b565 100644 --- a/src/main/webapp/app/overview/discussion-section/discussion-section.component.ts +++ b/src/main/webapp/app/overview/discussion-section/discussion-section.component.ts @@ -66,11 +66,16 @@ export class DiscussionSectionComponent extends CourseDiscussionDirective implem params: this.activatedRoute.params, queryParams: this.activatedRoute.queryParams, }).subscribe((routeParams: { params: Params; queryParams: Params }) => { + console.log(routeParams.params); this.currentPostId = +routeParams.queryParams.postId; this.course = this.exercise?.course ?? this.lecture?.course; this.metisService.setCourse(this.course); this.metisService.setPageType(this.pageType); - this.setChannel(routeParams.params.courseId); + if (routeParams.params.courseId) { + this.setChannel(routeParams.params.courseId); + } else if (this.course?.id) { + this.setChannel(this.course.id); + } this.createEmptyPost(); this.resetFormGroup(); }); diff --git a/src/main/webapp/i18n/de/competency.json b/src/main/webapp/i18n/de/competency.json index bd9fa4e9c696..74db1b570c43 100644 --- a/src/main/webapp/i18n/de/competency.json +++ b/src/main/webapp/i18n/de/competency.json @@ -168,6 +168,11 @@ "hide": "Lernpfad ausbleden", "show": "Lernpfad einblenden", "header": "Lernpfad" + }, + "participate": { + "noTaskSelected": "Aktuell hast du keine Vorlesungseinheit oder Aufgabe ausgewählt.", + "prev": "Zurück", + "next": "Nächste" } } } diff --git a/src/main/webapp/i18n/en/competency.json b/src/main/webapp/i18n/en/competency.json index bd778500d71f..3b7cb1d04568 100644 --- a/src/main/webapp/i18n/en/competency.json +++ b/src/main/webapp/i18n/en/competency.json @@ -167,6 +167,11 @@ "hide": "Hide Learning Path", "show": "Show Learning Path", "header": "Learning Path" + }, + "participate": { + "noTaskSelected": "Currently you have no lecture unit or exercise selected.", + "prev": "Previous", + "next": "Next" } } } diff --git a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java index 41388e7844b6..09bb6d32cccf 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java @@ -270,4 +270,31 @@ private static NgxLearningPathDTO.Node getNodeForExercise(Competency competency, return new NgxLearningPathDTO.Node(LearningPathService.getExerciseNodeId(competency.getId(), exercise.getId()), NgxLearningPathDTO.NodeType.EXERCISE, exercise.getId(), exercise.getTitle()); } + + @Nested + class RecommendationTest { + + @BeforeEach + void setAuthorizationForRepositoryRequests() { + SecurityUtils.setAuthorizationObject(); + } + + @Test + void testGetRecommendationEmpty() { + competencyUtilService.createCompetency(course); + LearningPath learningPath = learningPathUtilService.createLearningPathInCourse(course); + learningPath = learningPathRepository.findWithEagerCompetenciesAndLearningUnitsByIdElseThrow(learningPath.getId()); + assertThat(learningPathService.getRecommendation(learningPath)).isNull(); + } + + @Test + void testGetRecommendationNotEmpty() { + var competency = competencyUtilService.createCompetency(course); + final var lectureUnit = lectureUtilService.createTextUnit(); + competencyUtilService.linkLectureUnitToCompetency(competency, lectureUnit); + LearningPath learningPath = learningPathUtilService.createLearningPathInCourse(course); + learningPath = learningPathRepository.findWithEagerCompetenciesAndLearningUnitsByIdElseThrow(learningPath.getId()); + assertThat(learningPathService.getRecommendation(learningPath)).isNotNull(); + } + } } From a8d3fad104f55295fbfbd0fa88f649ceba7af3b3 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Mon, 17 Jul 2023 10:39:50 +0200 Subject: [PATCH 053/215] clean up --- .../learning-path-graph/learning-path-graph.component.html | 4 ++-- .../participate/learning-path-container.component.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.html b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.html index 7da8df136dd7..7ea68e33cc8f 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.html @@ -26,14 +26,14 @@ - {{ node.label }} asdff + {{ node.label }} - asdf + diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html index 1e146fa566c3..7f89d3c701aa 100644 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html @@ -3,7 +3,7 @@
-
+
From 454f7881b34b23cb15399f08f0279745dcc2c4c1 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Mon, 17 Jul 2023 16:27:41 +0200 Subject: [PATCH 054/215] update routing --- .../learning-paths/learning-paths.module.ts | 36 +++++++++------ .../learning-path-container.component.html | 19 ++------ .../learning-path-container.component.ts | 46 +++++++++++++------ ...ning-path-lecture-unit-view.component.html | 17 +++++++ ...arning-path-lecture-unit-view.component.ts | 21 +++++++++ .../learning-path-lecture-unit-view.module.ts | 33 +++++++++++++ ...ning-path-lecture-unit-view.component.html | 6 --- .../learning-path-lecture-unit-view.module.ts | 11 ----- .../course-exercise-details.component.ts | 10 ++-- 9 files changed, 135 insertions(+), 64 deletions(-) create mode 100644 src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.html rename src/main/webapp/app/course/learning-paths/participate/{lectureunit => lecture-unit}/learning-path-lecture-unit-view.component.ts (63%) create mode 100644 src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.module.ts delete mode 100644 src/main/webapp/app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.component.html delete mode 100644 src/main/webapp/app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.module.ts diff --git a/src/main/webapp/app/course/learning-paths/learning-paths.module.ts b/src/main/webapp/app/course/learning-paths/learning-paths.module.ts index 5d0740f39807..103e810f6a2a 100644 --- a/src/main/webapp/app/course/learning-paths/learning-paths.module.ts +++ b/src/main/webapp/app/course/learning-paths/learning-paths.module.ts @@ -11,7 +11,6 @@ import { LearningPathGraphSidebarComponent } from 'app/course/learning-paths/par import { LearningPathGraphComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph.component'; import { LearningPathGraphNodeComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph-node.component'; import { NgxGraphModule } from '@swimlane/ngx-graph'; -import { ArtemisLearningPathLectureUnitViewModule } from 'app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.module'; import { ArtemisLectureUnitsModule } from 'app/overview/course-lectures/lecture-units.module'; const routes: Routes = [ @@ -25,25 +24,36 @@ const routes: Routes = [ canActivate: [UserRouteAccessService], children: [ { - path: '', + path: 'lecture-unit', pathMatch: 'full', - loadChildren: () => import('app/overview/discussion-section/discussion-section.module').then((m) => m.DiscussionSectionModule), + children: [ + { + path: '', + pathMatch: 'full', + loadChildren: () => + import('app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.module').then( + (m) => m.ArtemisLearningPathLectureUnitViewModule, + ), + }, + ], + }, + { + path: 'exercise', + pathMatch: 'full', + children: [ + { + path: '', + pathMatch: 'full', + loadChildren: () => import('app/overview/exercise-details/course-exercise-details.module').then((m) => m.CourseExerciseDetailsModule), + }, + ], }, ], }, ]; @NgModule({ - imports: [ - ArtemisSharedModule, - FormsModule, - ReactiveFormsModule, - ArtemisSharedComponentModule, - NgxGraphModule, - ArtemisLearningPathLectureUnitViewModule, - RouterModule.forChild(routes), - ArtemisLectureUnitsModule, - ], + imports: [ArtemisSharedModule, FormsModule, ReactiveFormsModule, ArtemisSharedComponentModule, NgxGraphModule, RouterModule.forChild(routes), ArtemisLectureUnitsModule], declarations: [LearningPathContainerComponent, LearningPathManagementComponent, LearningPathGraphSidebarComponent, LearningPathGraphComponent, LearningPathGraphNodeComponent], exports: [LearningPathContainerComponent], }) diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html index 7f89d3c701aa..33169ba7befa 100644 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html @@ -4,23 +4,10 @@
-
- - -
- No task selected -
-
-
- +
+ No task selected
+
diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts index ee3f4b6d5e5d..0cad01cf985a 100644 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts @@ -2,7 +2,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons'; import { Exercise } from 'app/entities/exercise.model'; -import { LectureUnit, LectureUnitType } from 'app/entities/lecture-unit/lectureUnit.model'; +import { LectureUnit } from 'app/entities/lecture-unit/lectureUnit.model'; import { Lecture } from 'app/entities/lecture.model'; import { LearningPathService } from 'app/course/learning-paths/learning-path.service'; import { RecommendationType } from 'app/entities/learning-path.model'; @@ -10,8 +10,9 @@ import { LectureService } from 'app/lecture/lecture.service'; import { onError } from 'app/shared/util/global.utils'; import { HttpErrorResponse } from '@angular/common/http'; import { AlertService } from 'app/core/util/alert.service'; -import { isCommunicationEnabled, isMessagingEnabled } from 'app/entities/course.model'; -import { DiscussionSectionComponent } from 'app/overview/discussion-section/discussion-section.component'; +import { LearningPathLectureUnitViewComponent } from 'app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component'; +import { CourseExerciseDetailsComponent } from 'app/overview/exercise-details/course-exercise-details.component'; +import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; @Component({ selector: 'jhi-learning-path-container', @@ -33,14 +34,13 @@ export class LearningPathContainerComponent implements OnInit { faChevronLeft = faChevronLeft; faChevronRight = faChevronRight; - discussionComponent?: DiscussionSectionComponent; - constructor( private router: Router, private activatedRoute: ActivatedRoute, + private alertService: AlertService, private learningPathService: LearningPathService, private lectureService: LectureService, - private alertService: AlertService, + private exerciseService: ExerciseService, ) {} ngOnInit() { @@ -96,7 +96,6 @@ export class LearningPathContainerComponent implements OnInit { this.loadLectureUnit(); } } - console.log('request previous task'); } loadLectureUnit() { @@ -110,28 +109,45 @@ export class LearningPathContainerComponent implements OnInit { }, error: (errorResponse: HttpErrorResponse) => onError(this.alertService, errorResponse), }); + this.router.navigate(['lecture-unit'], { relativeTo: this.activatedRoute }); } loadExercise() { console.log('load exercise'); + this.exerciseService.getExerciseDetails(this.learningObjectId).subscribe({ + next: (exerciseResponse) => { + this.exercise = exerciseResponse.body!; + }, + error: (errorResponse: HttpErrorResponse) => onError(this.alertService, errorResponse), + }); + this.router.navigate(['exercise'], { relativeTo: this.activatedRoute }); } /** * This function gets called if the router outlet gets activated. This is - * used only for the DiscussionComponent + * used only for the LearningPathLectureUnitViewComponent * @param instance The component instance */ - onChildActivate(instance: DiscussionSectionComponent) { + onChildActivate(instance: LearningPathLectureUnitViewComponent | CourseExerciseDetailsComponent) { console.log(instance); - this.discussionComponent = instance; // save the reference to the component instance + if (instance instanceof LearningPathLectureUnitViewComponent) { + this.setupLectureUnitView(instance); + } else { + this.setupExerciseView(instance); + } + } + + setupLectureUnitView(instance: LearningPathLectureUnitViewComponent) { if (this.lecture) { instance.lecture = this.lecture; - instance.isCommunicationPage = false; + instance.lectureUnit = this.lectureUnit!; } - // todo exercise } - protected readonly isMessagingEnabled = isMessagingEnabled; - protected readonly isCommunicationEnabled = isCommunicationEnabled; - protected readonly LectureUnitType = LectureUnitType; + setupExerciseView(instance: CourseExerciseDetailsComponent) { + if (this.exercise) { + instance.courseId = this.courseId; + instance.exerciseId = this.learningObjectId; + } + } } diff --git a/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.html b/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.html new file mode 100644 index 000000000000..8002222110b2 --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.html @@ -0,0 +1,17 @@ +
+
+
+ + + + +
+
+
+ +
+
diff --git a/src/main/webapp/app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.component.ts b/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.ts similarity index 63% rename from src/main/webapp/app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.component.ts rename to src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.ts index af97c18bd9f7..97dee6226034 100644 --- a/src/main/webapp/app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.component.ts +++ b/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.ts @@ -5,6 +5,8 @@ import { onError } from 'app/shared/util/global.utils'; import { Lecture } from 'app/entities/lecture.model'; import { LectureUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/lectureUnit.service'; import { AlertService } from 'app/core/util/alert.service'; +import { isCommunicationEnabled, isMessagingEnabled } from 'app/entities/course.model'; +import { DiscussionSectionComponent } from 'app/overview/discussion-section/discussion-section.component'; export interface LectureUnitCompletionEvent { lectureUnit: LectureUnit; @@ -20,6 +22,8 @@ export class LearningPathLectureUnitViewComponent { @Input() lectureUnit: LectureUnit; readonly LectureUnitType = LectureUnitType; + discussionComponent?: DiscussionSectionComponent; + constructor(private lectureUnitService: LectureUnitService, private alertService: AlertService) {} completeLectureUnit(event: LectureUnitCompletionEvent): void { @@ -32,4 +36,21 @@ export class LearningPathLectureUnitViewComponent { }); } } + + protected readonly isMessagingEnabled = isMessagingEnabled; + protected readonly isCommunicationEnabled = isCommunicationEnabled; + + /** + * This function gets called if the router outlet gets activated. This is + * used only for the DiscussionComponent + * @param instance The component instance + */ + onChildActivate(instance: DiscussionSectionComponent) { + console.log(instance); + this.discussionComponent = instance; // save the reference to the component instance + if (this.lecture) { + instance.lecture = this.lecture; + instance.isCommunicationPage = false; + } + } } diff --git a/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.module.ts b/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.module.ts new file mode 100644 index 000000000000..33dc55941bda --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.module.ts @@ -0,0 +1,33 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { LearningPathLectureUnitViewComponent } from 'app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component'; +import { ArtemisLectureUnitsModule } from 'app/overview/course-lectures/lecture-units.module'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { Authority } from 'app/shared/constants/authority.constants'; +import { UserRouteAccessService } from 'app/core/auth/user-route-access-service'; + +const routes: Routes = [ + { + path: '', + component: LearningPathLectureUnitViewComponent, + data: { + authorities: [Authority.USER], + pageTitle: 'overview.learningPath', + }, + canActivate: [UserRouteAccessService], + children: [ + { + path: '', + pathMatch: 'full', + loadChildren: () => import('app/overview/discussion-section/discussion-section.module').then((m) => m.DiscussionSectionModule), + }, + ], + }, +]; + +@NgModule({ + imports: [ArtemisSharedModule, RouterModule.forChild(routes), ArtemisLectureUnitsModule], + declarations: [LearningPathLectureUnitViewComponent], + exports: [LearningPathLectureUnitViewComponent], +}) +export class ArtemisLearningPathLectureUnitViewModule {} diff --git a/src/main/webapp/app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.component.html b/src/main/webapp/app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.component.html deleted file mode 100644 index c6f4bcd378d1..000000000000 --- a/src/main/webapp/app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.component.html +++ /dev/null @@ -1,6 +0,0 @@ -
- - - - -
diff --git a/src/main/webapp/app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.module.ts b/src/main/webapp/app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.module.ts deleted file mode 100644 index 2e1f5bfc6c7d..000000000000 --- a/src/main/webapp/app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { NgModule } from '@angular/core'; -import { LearningPathLectureUnitViewComponent } from 'app/course/learning-paths/participate/lectureunit/learning-path-lecture-unit-view.component'; -import { ArtemisLectureUnitsModule } from 'app/overview/course-lectures/lecture-units.module'; -import { ArtemisSharedModule } from 'app/shared/shared.module'; - -@NgModule({ - imports: [ArtemisSharedModule, ArtemisLectureUnitsModule], - declarations: [LearningPathLectureUnitViewComponent], - exports: [LearningPathLectureUnitViewComponent], -}) -export class ArtemisLearningPathLectureUnitViewModule {} diff --git a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts index dfd01daa1ae6..d2f364805128 100644 --- a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts +++ b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts @@ -69,7 +69,7 @@ export class CourseExerciseDetailsComponent implements OnInit, OnDestroy { readonly isMessagingEnabled = isMessagingEnabled; private currentUser: User; - private exerciseId: number; + public exerciseId: number; public courseId: number; public course: Course; public exercise?: Exercise; @@ -142,8 +142,12 @@ export class CourseExerciseDetailsComponent implements OnInit, OnDestroy { this.route.params.subscribe((params) => { const didExerciseChange = this.exerciseId !== parseInt(params['exerciseId'], 10); const didCourseChange = this.courseId !== parseInt(params['courseId'], 10); - this.exerciseId = parseInt(params['exerciseId'], 10); - this.courseId = parseInt(params['courseId'], 10); + if (params['exerciseId']) { + this.exerciseId = parseInt(params['exerciseId'], 10); + } + if (params['courseId']) { + this.courseId = parseInt(params['courseId'], 10); + } this.courseService.find(this.courseId).subscribe((courseResponse) => (this.course = courseResponse.body!)); this.accountService.identity().then((user: User) => { this.currentUser = user; From 606e1360bc7d91afd2df70c0f37d26bd3efd5218 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Mon, 17 Jul 2023 16:59:18 +0200 Subject: [PATCH 055/215] move client test --- .../{ => management}/learning-path-management.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/test/javascript/spec/component/learning-paths/{ => management}/learning-path-management.component.spec.ts (99%) diff --git a/src/test/javascript/spec/component/learning-paths/learning-path-management.component.spec.ts b/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts similarity index 99% rename from src/test/javascript/spec/component/learning-paths/learning-path-management.component.spec.ts rename to src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts index 27d290f581c8..b2784bccf534 100644 --- a/src/test/javascript/spec/component/learning-paths/learning-path-management.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts @@ -4,7 +4,7 @@ import { LearningPathPagingService } from 'app/course/learning-paths/learning-pa import { SortService } from 'app/shared/service/sort.service'; import { PageableSearch, SearchResult, SortingOrder } from 'app/shared/table/pageable-table'; import { LearningPath } from 'app/entities/learning-path.model'; -import { ArtemisTestModule } from '../../test.module'; +import { ArtemisTestModule } from '../../../test.module'; import { MockComponent, MockDirective, MockProvider } from 'ng-mocks'; import { ButtonComponent } from 'app/shared/components/button.component'; import { NgbPagination } from '@ng-bootstrap/ng-bootstrap'; From 6e650d7a339e01d858838da847b99b3e4235a2fc Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Tue, 18 Jul 2023 11:23:35 +0200 Subject: [PATCH 056/215] fix html tag --- .../participate/learning-path-container.component.html | 2 +- .../participate/learning-path-graph-sidebar.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html index 33169ba7befa..1aaca5150412 100644 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html @@ -1,6 +1,6 @@
- +
diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.ts b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.ts index ab4e0de7ef6b..4005f86f7695 100644 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.ts +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.ts @@ -5,7 +5,7 @@ import { Course } from 'app/entities/course.model'; import { LearningPathGraphComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph.component'; @Component({ - selector: 'jhi-learning-path-sidebar', + selector: 'jhi-learning-path-graph-sidebar', styleUrls: ['./learning-path-graph-sidebar.component.scss'], templateUrl: './learning-path-graph-sidebar.component.html', }) From d72d5be28f515fdb0587e0d0ad3c22490ff5b065 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Wed, 19 Jul 2023 09:57:39 +0200 Subject: [PATCH 057/215] add tests --- .../learning-paths/learning-path.service.ts | 2 +- .../learning-path-container.component.spec.ts | 159 ++++++++++++++++++ ...rning-path-graph-sidebar.component.spec.ts | 50 ++++++ ...g-path-lecture-unit-view.component.spec.ts | 111 ++++++++++++ .../service/learning-path.service.spec.ts | 22 ++- 5 files changed, 342 insertions(+), 2 deletions(-) create mode 100644 src/test/javascript/spec/component/learning-paths/participate/learning-path-container.component.spec.ts create mode 100644 src/test/javascript/spec/component/learning-paths/participate/learning-path-graph-sidebar.component.spec.ts create mode 100644 src/test/javascript/spec/component/learning-paths/participate/learning-path-lecture-unit-view.component.spec.ts diff --git a/src/main/webapp/app/course/learning-paths/learning-path.service.ts b/src/main/webapp/app/course/learning-paths/learning-path.service.ts index ea5c3a3762c8..47727a8295bc 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path.service.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { HttpClient, HttpResponse } from '@angular/common/http'; import { Course } from 'app/entities/course.model'; -import { LearningPathRecommendation, NgxLearningPathDTO, RecommendationType } from 'app/entities/learning-path.model'; +import { LearningPathRecommendation, NgxLearningPathDTO } from 'app/entities/learning-path.model'; import { map } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) diff --git a/src/test/javascript/spec/component/learning-paths/participate/learning-path-container.component.spec.ts b/src/test/javascript/spec/component/learning-paths/participate/learning-path-container.component.spec.ts new file mode 100644 index 000000000000..860931670aa3 --- /dev/null +++ b/src/test/javascript/spec/component/learning-paths/participate/learning-path-container.component.spec.ts @@ -0,0 +1,159 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockComponent, MockModule } from 'ng-mocks'; +import { By } from '@angular/platform-browser'; +import { ArtemisTestModule } from '../../../test.module'; +import { of } from 'rxjs'; +import { ActivatedRoute, RouterModule } from '@angular/router'; +import { HttpResponse } from '@angular/common/http'; +import { LearningPathContainerComponent } from 'app/course/learning-paths/participate/learning-path-container.component'; +import { LearningPathService } from 'app/course/learning-paths/learning-path.service'; +import { LearningPathRecommendation, RecommendationType } from 'app/entities/learning-path.model'; +import { LectureService } from 'app/lecture/lecture.service'; +import { Lecture } from 'app/entities/lecture.model'; +import { LectureUnit } from 'app/entities/lecture-unit/lectureUnit.model'; +import { Exercise } from 'app/entities/exercise.model'; +import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; +import { LearningPathGraphSidebarComponent } from 'app/course/learning-paths/participate/learning-path-graph-sidebar.component'; +import { AttachmentUnit } from 'app/entities/lecture-unit/attachmentUnit.model'; +import { TextExercise } from 'app/entities/text-exercise.model'; + +describe('LearningPathContainerComponent', () => { + let fixture: ComponentFixture; + let comp: LearningPathContainerComponent; + let learningPathService: LearningPathService; + let getLearningPathIdStub: jest.SpyInstance; + const learningPathId = 1337; + let getRecommendationStub: jest.SpyInstance; + let lectureService: LectureService; + let lecture: Lecture; + let lectureUnit: LectureUnit; + let findWithDetailsStub: jest.SpyInstance; + let exerciseService: ExerciseService; + let exercise: Exercise; + let getExerciseDetailsStub: jest.SpyInstance; + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule, MockComponent(LearningPathGraphSidebarComponent), MockModule(RouterModule)], + declarations: [LearningPathContainerComponent], + providers: [ + { + provide: ActivatedRoute, + useValue: { + parent: { + parent: { + params: of({ + courseId: 1, + }), + }, + }, + }, + }, + ], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(LearningPathContainerComponent); + comp = fixture.componentInstance; + learningPathService = TestBed.inject(LearningPathService); + getLearningPathIdStub = jest.spyOn(learningPathService, 'getLearningPathId').mockReturnValue(of(new HttpResponse({ body: learningPathId }))); + getRecommendationStub = jest.spyOn(learningPathService, 'getRecommendation'); + + lectureUnit = new AttachmentUnit(); + lectureUnit.id = 3; + lecture = new Lecture(); + lecture.id = 2; + lecture.lectureUnits = [lectureUnit]; + lectureService = TestBed.inject(LectureService); + findWithDetailsStub = jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(of(new HttpResponse({ body: lecture }))); + + exercise = new TextExercise(undefined, undefined); + exercise.id = 4; + exerciseService = TestBed.inject(ExerciseService); + getExerciseDetailsStub = jest.spyOn(exerciseService, 'getExerciseDetails').mockReturnValue(of(new HttpResponse({ body: exercise }))); + + fixture.detectChanges(); + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should initialize', () => { + expect(comp.courseId).toBe(1); + expect(getLearningPathIdStub).toHaveBeenCalled(); + expect(getLearningPathIdStub).toHaveBeenCalledWith(1); + }); + + it('should request recommendation on next button click', () => { + const button = fixture.debugElement.query(By.css('.next-button')); + expect(button).not.toBeNull(); + button.nativeElement.click(); + expect(getRecommendationStub).toHaveBeenCalledWith(learningPathId); + }); + + it('should load lecture unit on recommendation', () => { + const recommendation = new LearningPathRecommendation(); + recommendation.learningObjectId = lectureUnit.id!; + recommendation.lectureId = lecture.id; + recommendation.type = RecommendationType.LECTURE_UNIT; + getRecommendationStub.mockReturnValue(of(new HttpResponse({ body: recommendation }))); + comp.onNextTask(); + expect(findWithDetailsStub).toHaveBeenCalled(); + expect(findWithDetailsStub).toHaveBeenCalledWith(lecture.id); + expect(getExerciseDetailsStub).not.toHaveBeenCalled(); + }); + + it('should load exercise on recommendation', () => { + const recommendation = new LearningPathRecommendation(); + recommendation.learningObjectId = exercise.id!; + recommendation.type = RecommendationType.EXERCISE; + getRecommendationStub.mockReturnValue(of(new HttpResponse({ body: recommendation }))); + comp.onNextTask(); + expect(findWithDetailsStub).not.toHaveBeenCalled(); + expect(getExerciseDetailsStub).toHaveBeenCalled(); + expect(getExerciseDetailsStub).toHaveBeenCalledWith(exercise.id); + }); + + it('should store current lecture unit in history', () => { + comp.learningObjectId = lectureUnit.id!; + comp.lectureUnit = lectureUnit; + comp.lectureId = lecture.id; + comp.lecture = lecture; + fixture.detectChanges(); + comp.onNextTask(); + expect(comp.history).toEqual([[lectureUnit.id!, lecture.id!]]); + }); + + it('should store current exercise in history', () => { + comp.learningObjectId = exercise.id!; + comp.exercise = exercise; + fixture.detectChanges(); + comp.onNextTask(); + expect(comp.history).toEqual([[exercise.id!, -1]]); + }); + + it('should load no previous task if history is empty', () => { + comp.onPrevTask(); + expect(findWithDetailsStub).not.toHaveBeenCalled(); + expect(getExerciseDetailsStub).not.toHaveBeenCalled(); + }); + + it('should load previous lecture unit', () => { + comp.history = [[lectureUnit.id!, lecture.id!]]; + fixture.detectChanges(); + comp.onPrevTask(); + expect(findWithDetailsStub).toHaveBeenCalled(); + expect(findWithDetailsStub).toHaveBeenCalledWith(lecture.id); + expect(getExerciseDetailsStub).not.toHaveBeenCalled(); + }); + + it('should load previous exercise', () => { + comp.history = [[exercise.id!, -1]]; + fixture.detectChanges(); + comp.onPrevTask(); + expect(findWithDetailsStub).not.toHaveBeenCalled(); + expect(getExerciseDetailsStub).toHaveBeenCalled(); + expect(getExerciseDetailsStub).toHaveBeenCalledWith(exercise.id); + }); +}); diff --git a/src/test/javascript/spec/component/learning-paths/participate/learning-path-graph-sidebar.component.spec.ts b/src/test/javascript/spec/component/learning-paths/participate/learning-path-graph-sidebar.component.spec.ts new file mode 100644 index 000000000000..f3be63c89f51 --- /dev/null +++ b/src/test/javascript/spec/component/learning-paths/participate/learning-path-graph-sidebar.component.spec.ts @@ -0,0 +1,50 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockComponent, MockPipe } from 'ng-mocks'; +import { By } from '@angular/platform-browser'; +import { ArtemisTestModule } from '../../../test.module'; +import { LearningPathGraphSidebarComponent } from 'app/course/learning-paths/participate/learning-path-graph-sidebar.component'; +import { LearningPathGraphComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph.component'; +import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { NgbTooltipMocksModule } from '../../../helpers/mocks/directive/ngbTooltipMocks.module'; + +describe('LearningPathGraphSidebarComponent', () => { + let fixture: ComponentFixture; + let comp: LearningPathGraphSidebarComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule, MockComponent(LearningPathGraphComponent), MockPipe(ArtemisTranslatePipe), NgbTooltipMocksModule], + declarations: [LearningPathGraphSidebarComponent], + providers: [], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(LearningPathGraphSidebarComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + }); + }); + + it('should create', () => { + expect(fixture).toBeTruthy(); + expect(comp).toBeTruthy(); + }); + + it('should show graph when not collapsed', () => { + comp.collapsed = false; + fixture.detectChanges(); + const expanded = fixture.debugElement.query(By.css('.expanded-graph')); + const collapsed = fixture.debugElement.query(By.css('.collapsed-graph')); + expect(expanded.nativeElement.hasAttribute('hidden')).toBeFalsy(); + expect(collapsed.nativeElement.hasAttribute('hidden')).toBeTruthy(); + }); + + it('should not show graph when collapsed', () => { + comp.collapsed = true; + fixture.detectChanges(); + const expanded = fixture.debugElement.query(By.css('.expanded-graph')); + const collapsed = fixture.debugElement.query(By.css('.collapsed-graph')); + expect(expanded.nativeElement.hasAttribute('hidden')).toBeTruthy(); + expect(collapsed.nativeElement.hasAttribute('hidden')).toBeFalsy(); + }); +}); diff --git a/src/test/javascript/spec/component/learning-paths/participate/learning-path-lecture-unit-view.component.spec.ts b/src/test/javascript/spec/component/learning-paths/participate/learning-path-lecture-unit-view.component.spec.ts new file mode 100644 index 000000000000..edf58948cd6b --- /dev/null +++ b/src/test/javascript/spec/component/learning-paths/participate/learning-path-lecture-unit-view.component.spec.ts @@ -0,0 +1,111 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockModule } from 'ng-mocks'; +import { ArtemisTestModule } from '../../../test.module'; +import { RouterModule } from '@angular/router'; +import { Lecture } from 'app/entities/lecture.model'; +import { LearningPathLectureUnitViewComponent } from 'app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component'; +import { AttachmentUnit } from 'app/entities/lecture-unit/attachmentUnit.model'; +import { ArtemisLectureUnitsModule } from 'app/overview/course-lectures/lecture-units.module'; +import { LectureUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/lectureUnit.service'; +import { VideoUnit } from 'app/entities/lecture-unit/videoUnit.model'; +import { TextUnit } from 'app/entities/lecture-unit/textUnit.model'; +import { OnlineUnit } from 'app/entities/lecture-unit/onlineUnit.model'; +import { Course, CourseInformationSharingConfiguration } from 'app/entities/course.model'; + +describe('LearningPathLectureUnitViewComponent', () => { + let fixture: ComponentFixture; + let comp: LearningPathLectureUnitViewComponent; + let lecture: Lecture; + let lectureUnitService: LectureUnitService; + let setCompletionStub: jest.SpyInstance; + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule, MockModule(RouterModule), MockModule(ArtemisLectureUnitsModule)], + declarations: [LearningPathLectureUnitViewComponent], + providers: [], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(LearningPathLectureUnitViewComponent); + comp = fixture.componentInstance; + lecture = new Lecture(); + lecture.id = 1; + lectureUnitService = TestBed.inject(LectureUnitService); + setCompletionStub = jest.spyOn(lectureUnitService, 'setCompletion'); + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should display attachment unit correctly', () => { + const attachment = new AttachmentUnit(); + attachment.id = 3; + lecture.lectureUnits = [attachment]; + comp.lecture = lecture; + comp.lectureUnit = attachment; + fixture.detectChanges(); + const view = fixture.debugElement.nativeElement.querySelector('jhi-attachment-unit'); + expect(view).toBeTruthy(); + }); + + it('should display video unit correctly', () => { + const video = new VideoUnit(); + video.id = 3; + lecture.lectureUnits = [video]; + comp.lecture = lecture; + comp.lectureUnit = video; + fixture.detectChanges(); + const view = fixture.debugElement.nativeElement.querySelector('jhi-video-unit'); + expect(view).toBeTruthy(); + }); + + it('should display text unit correctly', () => { + const text = new TextUnit(); + text.id = 3; + lecture.lectureUnits = [text]; + comp.lecture = lecture; + comp.lectureUnit = text; + fixture.detectChanges(); + const view = fixture.debugElement.nativeElement.querySelector('jhi-text-unit'); + expect(view).toBeTruthy(); + }); + + it('should display online unit correctly', () => { + const online = new OnlineUnit(); + online.id = 3; + lecture.lectureUnits = [online]; + comp.lecture = lecture; + comp.lectureUnit = online; + fixture.detectChanges(); + const view = fixture.debugElement.nativeElement.querySelector('jhi-online-unit'); + expect(view).toBeTruthy(); + }); + + it('should display no discussions when disabled', () => { + const attachment = new AttachmentUnit(); + attachment.id = 3; + lecture.lectureUnits = [attachment]; + comp.lecture = lecture; + comp.lectureUnit = attachment; + lecture.course = new Course(); + lecture.course.courseInformationSharingConfiguration = CourseInformationSharingConfiguration.DISABLED; + fixture.detectChanges(); + const outlet = fixture.debugElement.nativeElement.querySelector('router-outlet'); + expect(outlet).toBeFalsy(); + }); + + it('should display discussions when enabled', () => { + const attachment = new AttachmentUnit(); + attachment.id = 3; + lecture.lectureUnits = [attachment]; + comp.lecture = lecture; + comp.lectureUnit = attachment; + lecture.course = new Course(); + lecture.course.courseInformationSharingConfiguration = CourseInformationSharingConfiguration.COMMUNICATION_AND_MESSAGING; + fixture.detectChanges(); + const outlet = fixture.debugElement.nativeElement.querySelector('router-outlet'); + expect(outlet).toBeTruthy(); + }); +}); diff --git a/src/test/javascript/spec/service/learning-path.service.spec.ts b/src/test/javascript/spec/service/learning-path.service.spec.ts index 0f7241ac4099..e6cae7839aa7 100644 --- a/src/test/javascript/spec/service/learning-path.service.spec.ts +++ b/src/test/javascript/spec/service/learning-path.service.spec.ts @@ -8,6 +8,7 @@ describe('LearningPathService', () => { let learningPathService: LearningPathService; let httpService: HttpClient; let putStub: jest.SpyInstance; + let getStub: jest.SpyInstance; beforeEach(() => { TestBed.configureTestingModule({ @@ -19,6 +20,7 @@ describe('LearningPathService', () => { httpService = TestBed.inject(HttpClient); learningPathService = new LearningPathService(httpService); putStub = jest.spyOn(httpService, 'put'); + getStub = jest.spyOn(httpService, 'get'); }); }); @@ -26,9 +28,27 @@ describe('LearningPathService', () => { jest.restoreAllMocks(); }); - it('should send a request to the server to activate the user', () => { + it('should send a request to the server to enable learning paths for course', () => { learningPathService.enableLearningPaths(1).subscribe(); expect(putStub).toHaveBeenCalledOnce(); expect(putStub).toHaveBeenCalledWith('api/courses/1/learning-paths/enable', null, { observe: 'response' }); }); + + it('should send a request to the server to get ngx representation of learning path', () => { + learningPathService.getNgxLearningPath(1).subscribe(); + expect(getStub).toHaveBeenCalledOnce(); + expect(getStub).toHaveBeenCalledWith('api/courses/1/learning-path-graph', { observe: 'response' }); + }); + + it('should send a request to the server to get learning path id of the current user in the course', () => { + learningPathService.getLearningPathId(1).subscribe(); + expect(getStub).toHaveBeenCalledOnce(); + expect(getStub).toHaveBeenCalledWith('api/courses/1/learning-path-id', { observe: 'response' }); + }); + + it('should send a request to the server to get recommendation for learning path', () => { + learningPathService.getRecommendation(1).subscribe(); + expect(getStub).toHaveBeenCalledOnce(); + expect(getStub).toHaveBeenCalledWith('api/learning-path/1/recommendation', { observe: 'response' }); + }); }); From 7e995f666b7b4a65861dd989795e8d7f95505ce4 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Wed, 19 Jul 2023 13:47:37 +0200 Subject: [PATCH 058/215] add graph min-size --- .../participate/learning-path-graph-sidebar.component.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.scss b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.scss index 9b4ce9f778d7..38d1584c12c5 100644 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.scss +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.scss @@ -7,6 +7,7 @@ $graph-min-width: 215px; .expanded-graph { display: flex; width: calc(#{$draggable-width} + #{$graph-min-width}); + min-height: 500px; margin-left: auto; .scrollbar { From f01d566071cc953e7da251336e547ff860dc05cc Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 20 Jul 2023 12:15:19 +0200 Subject: [PATCH 059/215] improve endpoints and rendering --- package-lock.json | 10 ---- package.json | 3 +- .../artemis/service/LearningPathService.java | 24 +++++----- .../web/rest/LearningPathResource.java | 48 +++++++++++-------- .../learning-path-graph.component.html | 12 ++--- .../learning-path-graph.component.ts | 15 ++---- .../learning-paths/learning-path.service.ts | 12 ++--- .../learning-path-container.component.html | 2 +- .../learning-path-container.component.ts | 9 +--- ...learning-path-graph-sidebar.component.html | 3 +- .../learning-path-graph-sidebar.component.ts | 8 +--- .../lecture/LearningPathIntegrationTest.java | 41 +++++++++++++--- .../service/learning-path.service.spec.ts | 12 ++--- 13 files changed, 104 insertions(+), 95 deletions(-) diff --git a/package-lock.json b/package-lock.json index 74613708b522..6f16c5ae6515 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5662,11 +5662,6 @@ "d3-time-format": "2 - 3" } }, - "node_modules/@swimlane/ngx-graph/node_modules/d3-selection": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.2.tgz", - "integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==" - }, "node_modules/@swimlane/ngx-graph/node_modules/d3-shape": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", @@ -19848,11 +19843,6 @@ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" }, - "node_modules/webcola/node_modules/d3-selection": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.2.tgz", - "integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==" - }, "node_modules/webcola/node_modules/d3-shape": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", diff --git a/package.json b/package.json index 02d5cff55b8f..0830fa3fe281 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,8 @@ "d3-color": "^3.1.0", "d3-interpolate": "^3.0.1", "d3-transition": "^3.0.1", - "d3-brush": "^3.0.0" + "d3-brush": "^3.0.0", + "d3-selection": "^3.0.0" }, "semver": "7.5.3", "word-wrap": "1.2.3" diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index fdeb726167c7..a57354c217fc 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -327,19 +327,19 @@ private void generateNgxRepresentationForRelation(CompetencyRelation relation, M } public static String getCompetencyStartNodeId(long competencyId) { - return competencyId + "-start"; + return "node-" + competencyId + "-start"; } public static String getCompetencyEndNodeId(long competencyId) { - return competencyId + "-end"; + return "node-" + competencyId + "-end"; } public static String getLectureUnitNodeId(long competencyId, long lectureUnitId) { - return competencyId + "-lu-" + lectureUnitId; + return "node-" + competencyId + "-lu-" + lectureUnitId; } public static String getExerciseNodeId(long competencyId, long exerciseId) { - return competencyId + "-ex-" + exerciseId; + return "node-" + competencyId + "-ex-" + exerciseId; } public static String getMatchingClusterStartNodeId(long matchingClusterId) { @@ -351,35 +351,35 @@ public static String getMatchingClusterEndNodeId(long matchingClusterId) { } public static String getLectureUnitInEdgeId(long competencyId, long lectureUnitId) { - return competencyId + "-lu-" + getInEdgeId(lectureUnitId); + return "edge-" + competencyId + "-lu-" + getInEdgeId(lectureUnitId); } public static String getLectureUnitOutEdgeId(long competencyId, long lectureUnitId) { - return competencyId + "-lu-" + getOutEdgeId(lectureUnitId); + return "edge-" + competencyId + "-lu-" + getOutEdgeId(lectureUnitId); } public static String getExerciseInEdgeId(long competencyId, long exercise) { - return competencyId + "-ex-" + getInEdgeId(exercise); + return "edge-" + competencyId + "-ex-" + getInEdgeId(exercise); } public static String getExerciseOutEdgeId(long competencyId, long exercise) { - return competencyId + "-ex-" + getOutEdgeId(exercise); + return "edge-" + competencyId + "-ex-" + getOutEdgeId(exercise); } public static String getInEdgeId(long id) { - return id + "-in"; + return "edge-" + id + "-in"; } public static String getOutEdgeId(long id) { - return id + "-out"; + return "edge-" + id + "-out"; } public static String getRelationEdgeId(String sourceNodeId, String targetNodeId) { - return "relation-" + sourceNodeId + "-" + targetNodeId; + return "edge-relation-" + sourceNodeId + "-" + targetNodeId; } public static String getDirectEdgeId(long competencyId) { - return competencyId + "-direct"; + return "edge-" + competencyId + "-direct"; } public LearningObject getRecommendation(LearningPath learningPath) { diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java index 2f9b21d39290..761aaf308b12 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java @@ -22,6 +22,7 @@ import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; import de.tum.in.www1.artemis.web.rest.dto.learningpath.LearningPathRecommendation; import de.tum.in.www1.artemis.web.rest.dto.learningpath.NgxLearningPathDTO; +import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException; @RestController @RequestMapping("/api") @@ -95,44 +96,51 @@ public ResponseEntity> getLearningPathsOnPage( } /** - * GET /courses/:courseId/learning-path-graph : Gets the ngx representation of the learning path. + * GET /courses/:courseId/learning-path-id : Gets the id of the learning path. * - * @param courseId the id of the course from which the learning path should be fetched - * @return the ResponseEntity with status 200 (OK) and with body the ngx representation of the learning path + * @param courseId the id of the course from which the learning path id should be fetched + * @return the ResponseEntity with status 200 (OK) and with body the id of the learning path */ - @GetMapping("/courses/{courseId}/learning-path-graph") + @GetMapping("/courses/{courseId}/learning-path-id") @EnforceAtLeastStudent - public ResponseEntity getNgxLearningPath(@PathVariable Long courseId) { - log.debug("REST request to get ngx representation of learning path for course with id: {}", courseId); + public ResponseEntity getLearningPathId(@PathVariable Long courseId) { + log.debug("REST request to get learning path id for course with id: {}", courseId); Course course = courseRepository.findByIdElseThrow(courseId); User user = userRepository.getUserWithGroupsAndAuthorities(); authorizationCheckService.isStudentInCourse(course, user); if (!course.getLearningPathsEnabled()) { throw new BadRequestException("Learning paths are not enabled for this course."); } - LearningPath learningPath = learningPathRepository.findWithEagerCompetenciesAndLearningUnitsByCourseIdAndUserIdElseThrow(course.getId(), user.getId()); - NgxLearningPathDTO graph = learningPathService.generateNgxRepresentation(learningPath); - return ResponseEntity.ok(graph); + LearningPath learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), user.getId()); + return ResponseEntity.ok(learningPath.getId()); } /** - * GET /courses/:courseId/learning-path-id : Gets the id of the learning path. + * GET /learning-path/:learningPathId : Gets the ngx representation of the learning path. * - * @param courseId the id of the course from which the learning path id should be fetched - * @return the ResponseEntity with status 200 (OK) and with body the id of the learning path + * @param learningPathId the id of the learning path that should be fetched + * @return the ResponseEntity with status 200 (OK) and with body the ngx representation of the learning path */ - @GetMapping("/courses/{courseId}/learning-path-id") + @GetMapping("/learning-path/{learningPathId}") @EnforceAtLeastStudent - public ResponseEntity getLearningPathId(@PathVariable Long courseId) { - log.debug("REST request to get learning path id for course with id: {}", courseId); - Course course = courseRepository.findByIdElseThrow(courseId); - User user = userRepository.getUserWithGroupsAndAuthorities(); - authorizationCheckService.isStudentInCourse(course, user); + public ResponseEntity getNgxLearningPath(@PathVariable Long learningPathId) { + log.debug("REST request to get ngx representation of learning path with id: {}", learningPathId); + LearningPath learningPath = learningPathRepository.findWithEagerCompetenciesAndLearningUnitsByIdElseThrow(learningPathId); + Course course = courseRepository.findByIdElseThrow(learningPath.getCourse().getId()); if (!course.getLearningPathsEnabled()) { throw new BadRequestException("Learning paths are not enabled for this course."); } - LearningPath learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), user.getId()); - return ResponseEntity.ok(learningPath.getId()); + User user = userRepository.getUserWithGroupsAndAuthorities(); + if (authorizationCheckService.isStudentInCourse(course, user)) { + if (!user.getId().equals(learningPath.getUser().getId())) { + throw new AccessForbiddenException("You are not allowed to access another users learning path."); + } + } + else if (!authorizationCheckService.isAtLeastInstructorInCourse(course, user) && !authorizationCheckService.isAdmin()) { + throw new AccessForbiddenException("You are not allowed to access another users learning path."); + } + NgxLearningPathDTO graph = learningPathService.generateNgxRepresentation(learningPath); + return ResponseEntity.ok(graph); } /** diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.html b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.html index 7ea68e33cc8f..9a8f5baff820 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.html @@ -22,18 +22,16 @@ - - - - - {{ node.label }} + + + - + - + text diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts index 9731392cff7b..cc323aced122 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnChanges, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Layout } from '@swimlane/ngx-graph'; import * as shape from 'd3-shape'; @@ -13,6 +13,7 @@ import { NgxLearningPathDTO } from 'app/entities/learning-path.model'; }) export class LearningPathGraphComponent implements OnInit { isLoading = false; + @Input() learningPathId: number; @Input() courseId: number; ngxLearningPath: NgxLearningPathDTO; @@ -31,23 +32,17 @@ export class LearningPathGraphComponent implements OnInit { constructor(private activatedRoute: ActivatedRoute, private learningPathService: LearningPathService) {} ngOnInit() { - if (!this.courseId) { - this.activatedRoute.parent!.parent!.params.subscribe((params) => { - this.courseId = +params['courseId']; - if (this.courseId) { - this.loadData(); - } - }); - } else { + if (this.learningPathId) { this.loadData(); } } loadData() { this.isLoading = true; - this.learningPathService.getNgxLearningPath(this.courseId).subscribe((ngxLearningPathResponse) => { + this.learningPathService.getNgxLearningPath(this.learningPathId).subscribe((ngxLearningPathResponse) => { this.ngxLearningPath = ngxLearningPathResponse.body!; this.isLoading = false; + console.log(this.ngxLearningPath); }); } diff --git a/src/main/webapp/app/course/learning-paths/learning-path.service.ts b/src/main/webapp/app/course/learning-paths/learning-path.service.ts index 47727a8295bc..75f4f7ed6bc4 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path.service.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path.service.ts @@ -15,8 +15,12 @@ export class LearningPathService { return this.httpClient.put(`${this.resourceURL}/courses/${courseId}/learning-paths/enable`, null, { observe: 'response' }); } - getNgxLearningPath(courseId: number): Observable> { - return this.httpClient.get(`${this.resourceURL}/courses/${courseId}/learning-path-graph`, { observe: 'response' }).pipe( + getLearningPathId(courseId: number) { + return this.httpClient.get(`${this.resourceURL}/courses/${courseId}/learning-path-id`, { observe: 'response' }); + } + + getNgxLearningPath(learningPathId: number): Observable> { + return this.httpClient.get(`${this.resourceURL}/learning-path/${learningPathId}`, { observe: 'response' }).pipe( map((ngxLearningPathResponse) => { if (!ngxLearningPathResponse.body!.nodes) { ngxLearningPathResponse.body!.nodes = []; @@ -32,10 +36,6 @@ export class LearningPathService { ); } - getLearningPathId(courseId: number) { - return this.httpClient.get(`${this.resourceURL}/courses/${courseId}/learning-path-id`, { observe: 'response' }); - } - getRecommendation(learningPathId: number) { return this.httpClient.get(`${this.resourceURL}/learning-path/${learningPathId}/recommendation`, { observe: 'response' }); } diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html index 1aaca5150412..8c24c96375eb 100644 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html @@ -1,6 +1,6 @@
- +
diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts index 0cad01cf985a..11285c292c05 100644 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts @@ -19,8 +19,7 @@ import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service' templateUrl: './learning-path-container.component.html', }) export class LearningPathContainerComponent implements OnInit { - @Input() - courseId: number; + @Input() courseId: number; learningPathId: number; learningObjectId: number; @@ -44,7 +43,6 @@ export class LearningPathContainerComponent implements OnInit { ) {} ngOnInit() { - console.log('ON INIT CONTAINER'); if (!this.courseId) { this.activatedRoute.parent!.parent!.params.subscribe((params) => { this.courseId = +params['courseId']; @@ -52,6 +50,7 @@ export class LearningPathContainerComponent implements OnInit { } this.learningPathService.getLearningPathId(this.courseId).subscribe((learningPathIdResponse) => { this.learningPathId = learningPathIdResponse.body!; + console.log('container' + this.learningPathId); }); } @@ -62,8 +61,6 @@ export class LearningPathContainerComponent implements OnInit { this.history.push([this.exercise.id, -1]); } this.undefineAll(); - console.log('request next task'); - console.log(this.courseId); this.learningPathService.getRecommendation(this.learningPathId).subscribe((recommendationResponse) => { const recommendation = recommendationResponse.body!; this.learningObjectId = recommendation.learningObjectId; @@ -99,7 +96,6 @@ export class LearningPathContainerComponent implements OnInit { } loadLectureUnit() { - console.log('loading lecture unit'); this.lectureService.findWithDetails(this.lectureId!).subscribe({ next: (findLectureResult) => { this.lecture = findLectureResult.body!; @@ -113,7 +109,6 @@ export class LearningPathContainerComponent implements OnInit { } loadExercise() { - console.log('load exercise'); this.exerciseService.getExerciseDetails(this.learningObjectId).subscribe({ next: (exerciseResponse) => { this.exercise = exerciseResponse.body!; diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.html b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.html index 1cadb8f0904d..08a8a91621f6 100644 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.html +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.html @@ -13,8 +13,7 @@

- - +
diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.ts b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.ts index 4005f86f7695..cf6ff5060559 100644 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.ts +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.ts @@ -1,7 +1,6 @@ -import { AfterViewInit, Component, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, Input, ViewChild } from '@angular/core'; import interact from 'interactjs'; import { faChevronLeft, faChevronRight, faGripLinesVertical, faNetworkWired } from '@fortawesome/free-solid-svg-icons'; -import { Course } from 'app/entities/course.model'; import { LearningPathGraphComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph.component'; @Component({ @@ -10,8 +9,7 @@ import { LearningPathGraphComponent } from 'app/course/learning-paths/learning-p templateUrl: './learning-path-graph-sidebar.component.html', }) export class LearningPathGraphSidebarComponent implements AfterViewInit { - course?: Course; - + @Input() learningPathId: number; collapsed: boolean; // Icons faChevronLeft = faChevronLeft; @@ -22,8 +20,6 @@ export class LearningPathGraphSidebarComponent implements AfterViewInit { @ViewChild(`learningPathGraphComponent`, { static: false }) learningPathGraphComponent: LearningPathGraphComponent; - constructor() {} - ngAfterViewInit(): void { // allows the conversation sidebar to be resized towards the right-hand side interact('.expanded-graph') diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java index b0c0c95cadee..49e9590407cd 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java @@ -28,7 +28,6 @@ import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.service.CompetencyProgressService; -import de.tum.in.www1.artemis.service.LearningPathService; import de.tum.in.www1.artemis.service.LectureUnitService; import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.util.PageableSearchUtilService; @@ -44,9 +43,6 @@ class LearningPathIntegrationTest extends AbstractSpringIntegrationBambooBitbuck @Autowired private UserRepository userRepository; - @Autowired - private LearningPathService learningPathService; - @Autowired private UserUtilService userUtilService; @@ -354,7 +350,21 @@ void testUpdateLearningPathProgress() throws Exception { @Test @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") void testGetNgxLearningPathForLearningPathsDisabled() throws Exception { - request.get("/api/courses/" + course.getId() + "/learning-path-graph", HttpStatus.BAD_REQUEST, NgxLearningPathDTO.class); + course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); + final var student = userRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); + course.setLeanringPathsEnabled(false); + courseRepository.save(course); + request.get("/api/learning-path/" + learningPath.getId(), HttpStatus.BAD_REQUEST, NgxLearningPathDTO.class); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student2", roles = "USER") + void testGetNgxLearningPathForOtherStudent() throws Exception { + course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); + final var student = userRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); + request.get("/api/learning-path/" + learningPath.getId(), HttpStatus.FORBIDDEN, NgxLearningPathDTO.class); } /** @@ -365,8 +375,25 @@ void testGetNgxLearningPathForLearningPathsDisabled() throws Exception { */ @Test @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") - void testGetNgxLearningPath() throws Exception { + void testGetNgxLearningPathAsStudent() throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); - request.get("/api/courses/" + course.getId() + "/learning-path-graph", HttpStatus.OK, NgxLearningPathDTO.class); + final var student = userRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); + request.get("/api/learning-path/" + learningPath.getId(), HttpStatus.OK, NgxLearningPathDTO.class); + } + + /** + * This only tests if the end point successfully retrieves the graph representation. The correctness of the response is tested in LearningPathServiceTest. + * + * @throws Exception the request failed + * @see de.tum.in.www1.artemis.service.LearningPathServiceTest + */ + @Test + @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") + void testGetNgxLearningPathAsInstructor() throws Exception { + course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); + final var student = userRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); + request.get("/api/learning-path/" + learningPath.getId(), HttpStatus.OK, NgxLearningPathDTO.class); } } diff --git a/src/test/javascript/spec/service/learning-path.service.spec.ts b/src/test/javascript/spec/service/learning-path.service.spec.ts index e6cae7839aa7..6748f72a1fad 100644 --- a/src/test/javascript/spec/service/learning-path.service.spec.ts +++ b/src/test/javascript/spec/service/learning-path.service.spec.ts @@ -34,18 +34,18 @@ describe('LearningPathService', () => { expect(putStub).toHaveBeenCalledWith('api/courses/1/learning-paths/enable', null, { observe: 'response' }); }); - it('should send a request to the server to get ngx representation of learning path', () => { - learningPathService.getNgxLearningPath(1).subscribe(); - expect(getStub).toHaveBeenCalledOnce(); - expect(getStub).toHaveBeenCalledWith('api/courses/1/learning-path-graph', { observe: 'response' }); - }); - it('should send a request to the server to get learning path id of the current user in the course', () => { learningPathService.getLearningPathId(1).subscribe(); expect(getStub).toHaveBeenCalledOnce(); expect(getStub).toHaveBeenCalledWith('api/courses/1/learning-path-id', { observe: 'response' }); }); + it('should send a request to the server to get ngx representation of learning path', () => { + learningPathService.getNgxLearningPath(1).subscribe(); + expect(getStub).toHaveBeenCalledOnce(); + expect(getStub).toHaveBeenCalledWith('api/learning-path/1', { observe: 'response' }); + }); + it('should send a request to the server to get recommendation for learning path', () => { learningPathService.getRecommendation(1).subscribe(); expect(getStub).toHaveBeenCalledOnce(); From c1d3aaf01365aaa2927b941d9d65ca3d1e20f9b4 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 20 Jul 2023 12:49:34 +0200 Subject: [PATCH 060/215] extend Ngx model --- .../artemis/service/LearningPathService.java | 9 ++++----- .../dto/learningpath/NgxLearningPathDTO.java | 20 +++++++++++++++++-- .../app/entities/learning-path.model.ts | 1 + 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index a57354c217fc..571d45258c84 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -203,10 +203,9 @@ private void generateNgxRepresentationForCompetency(Competency competency, Set { currentCluster.add(new NgxLearningPathDTO.Node(getLectureUnitNodeId(competency.getId(), lectureUnit.getId()), NgxLearningPathDTO.NodeType.LECTURE_UNIT, @@ -284,8 +283,8 @@ private void generateNgxRepresentationForRelations(LearningPath learningPath, Se // generate match cluster start and end nodes for (int i = 0; i < matchClusters.numberOfSets(); i++) { - nodes.add(new NgxLearningPathDTO.Node(getMatchingClusterStartNodeId(i), NgxLearningPathDTO.NodeType.COMPETENCY_START, -1, "")); - nodes.add(new NgxLearningPathDTO.Node(getMatchingClusterEndNodeId(i), NgxLearningPathDTO.NodeType.COMPETENCY_END, -1, "")); + nodes.add(new NgxLearningPathDTO.Node(getMatchingClusterStartNodeId(i), NgxLearningPathDTO.NodeType.COMPETENCY_START)); + nodes.add(new NgxLearningPathDTO.Node(getMatchingClusterEndNodeId(i), NgxLearningPathDTO.NodeType.COMPETENCY_END)); } // generate edges between match cluster nodes and corresponding competencies diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/NgxLearningPathDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/NgxLearningPathDTO.java index 08606e88c07f..90ca65a77b43 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/NgxLearningPathDTO.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/NgxLearningPathDTO.java @@ -23,7 +23,23 @@ public String toString() { return "NgxLearningPathDTO{nodes=" + nodes + ", edges=" + edges + ", clusters=" + clusters + "}"; } - public record Node(String id, NodeType type, long linkedResource, String label) { + public record Node(String id, NodeType type, long linkedResource, boolean completed, String label) { + + public Node(String id, NodeType type, long linkedResource, String label) { + this(id, type, linkedResource, false, label); + } + + public Node(String id, NodeType type, String label) { + this(id, type, -1, label); + } + + public Node(String id, NodeType type, long linkedResource) { + this(id, type, linkedResource, ""); + } + + public Node(String id, NodeType type) { + this(id, type, -1); + } @Override public boolean equals(Object obj) { @@ -35,7 +51,7 @@ public boolean equals(Object obj) { @Override public String toString() { - return "Node{id=" + id + ", type=" + type.name() + ", linkedResource=" + linkedResource + ", label=" + label + "}"; + return "Node{id=" + id + ", type=" + type.name() + ", linkedResource=" + linkedResource + ", completed=" + completed + ", label=" + label + "}"; } } diff --git a/src/main/webapp/app/entities/learning-path.model.ts b/src/main/webapp/app/entities/learning-path.model.ts index dc2390022442..b6573c8d0fb2 100644 --- a/src/main/webapp/app/entities/learning-path.model.ts +++ b/src/main/webapp/app/entities/learning-path.model.ts @@ -24,6 +24,7 @@ export class NgxLearningPathNode implements Node { public id: string; public type?: NodeType; public linkedResource?: number; + public completed?: boolean; public label?: string; } From 197efb8f72dce36988ad85260302596e055cede6 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 20 Jul 2023 15:53:53 +0200 Subject: [PATCH 061/215] fix rendering of completed learning objects --- .../repository/LearningPathRepository.java | 23 ++++++++++--------- .../artemis/service/LearningPathService.java | 17 +++++++------- .../web/rest/LearningPathResource.java | 4 ++-- .../learning-path-graph-node.component.html | 9 +++++--- .../learning-path-graph-node.component.ts | 17 +++++--------- .../learning-path-graph.component.html | 7 ++---- .../learning-path-graph.component.scss | 6 +++++ .../app/entities/learning-path.model.ts | 12 +++++----- .../service/LearningPathServiceTest.java | 6 ++--- 9 files changed, 52 insertions(+), 49 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java index 295d2b5ba610..4a08dfcc341a 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java @@ -34,14 +34,6 @@ default LearningPath findWithEagerCompetenciesByCourseIdAndUserIdElseThrow(long return findWithEagerCompetenciesByCourseIdAndUserId(courseId, userId).orElseThrow(() -> new EntityNotFoundException("LearningPath")); } - @EntityGraph(type = LOAD, attributePaths = { "competencies", "competencies.lectureUnits", "competencies.exercises" }) - Optional findWithEagerCompetenciesAndLearningUnitsByCourseIdAndUserId(long courseId, long userId); - - @NotNull - default LearningPath findWithEagerCompetenciesAndLearningUnitsByCourseIdAndUserIdElseThrow(long courseId, long userId) { - return findWithEagerCompetenciesAndLearningUnitsByCourseIdAndUserId(courseId, userId).orElseThrow(() -> new EntityNotFoundException("LearningPath")); - } - @Query(""" SELECT lp FROM LearningPath lp @@ -65,10 +57,19 @@ default LearningPath findWithEagerCompetenciesByIdElseThrow(long learningPathId) } @EntityGraph(type = LOAD, attributePaths = { "competencies", "competencies.lectureUnits", "competencies.exercises" }) - Optional findWithEagerCompetenciesAndLearningUnitsById(long learningPathId); + Optional findWithEagerCompetenciesAndLearningObjectsById(long learningPathId); + + @NotNull + default LearningPath findWithEagerCompetenciesAndLearningObjectsByIdElseThrow(long learningPathId) { + return findWithEagerCompetenciesAndLearningObjectsById(learningPathId).orElseThrow(() -> new EntityNotFoundException("LearningPath", learningPathId)); + } + + @EntityGraph(type = LOAD, attributePaths = { "competencies", "competencies.lectureUnits", "competencies.lectureUnits.completedUsers", "competencies.exercises", + "competencies.exercises.studentParticipations" }) + Optional findWithEagerCompetenciesAndLearningObjectsAndCompletedUsersById(long learningPathId); @NotNull - default LearningPath findWithEagerCompetenciesAndLearningUnitsByIdElseThrow(long learningPathId) { - return findWithEagerCompetenciesAndLearningUnitsById(learningPathId).orElseThrow(() -> new EntityNotFoundException("LearningPath", learningPathId)); + default LearningPath findWithEagerCompetenciesAndLearningObjectsAndCompletedUsersByIdElseThrow(long learningPathId) { + return findWithEagerCompetenciesAndLearningObjectsAndCompletedUsersById(learningPathId).orElseThrow(() -> new EntityNotFoundException("LearningPath", learningPathId)); } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index 571d45258c84..9b1c5509f479 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -174,7 +174,7 @@ public NgxLearningPathDTO generateNgxRepresentation(LearningPath learningPath) { Set nodes = new HashSet<>(); Set edges = new HashSet<>(); Set clusters = new HashSet<>(); - learningPath.getCompetencies().forEach(competency -> generateNgxRepresentationForCompetency(competency, nodes, edges, clusters)); + learningPath.getCompetencies().forEach(competency -> generateNgxRepresentationForCompetency(learningPath, competency, nodes, edges, clusters)); generateNgxRepresentationForRelations(learningPath, nodes, edges); return new NgxLearningPathDTO(nodes, edges, clusters); } @@ -192,12 +192,13 @@ public NgxLearningPathDTO generateNgxRepresentation(LearningPath learningPath) { *
  • a cluster consisting of all created nodes
  • * * - * @param competency the competency for which the representation will be created - * @param nodes set of nodes to store the new nodes - * @param edges set of edges to store the new edges - * @param clusters set of clusters to store the new clusters + * @param learningPath the learning path for which the representation should be created + * @param competency the competency for which the representation will be created + * @param nodes set of nodes to store the new nodes + * @param edges set of edges to store the new edges + * @param clusters set of clusters to store the new clusters */ - private void generateNgxRepresentationForCompetency(Competency competency, Set nodes, Set edges, + private void generateNgxRepresentationForCompetency(LearningPath learningPath, Competency competency, Set nodes, Set edges, Set clusters) { Set currentCluster = new HashSet<>(); // generates start and end node @@ -209,7 +210,7 @@ private void generateNgxRepresentationForCompetency(Competency competency, Set { currentCluster.add(new NgxLearningPathDTO.Node(getLectureUnitNodeId(competency.getId(), lectureUnit.getId()), NgxLearningPathDTO.NodeType.LECTURE_UNIT, - lectureUnit.getId(), lectureUnit.getName())); + lectureUnit.getId(), lectureUnit.isCompletedFor(learningPath.getUser()), lectureUnit.getName())); edges.add(new NgxLearningPathDTO.Edge(getLectureUnitInEdgeId(competency.getId(), lectureUnit.getId()), startNodeId, getLectureUnitNodeId(competency.getId(), lectureUnit.getId()))); edges.add(new NgxLearningPathDTO.Edge(getLectureUnitOutEdgeId(competency.getId(), lectureUnit.getId()), getLectureUnitNodeId(competency.getId(), lectureUnit.getId()), @@ -218,7 +219,7 @@ private void generateNgxRepresentationForCompetency(Competency competency, Set { currentCluster.add(new NgxLearningPathDTO.Node(getExerciseNodeId(competency.getId(), exercise.getId()), NgxLearningPathDTO.NodeType.EXERCISE, exercise.getId(), - exercise.getTitle())); + exercise.isCompletedFor(learningPath.getUser()), exercise.getTitle())); edges.add(new NgxLearningPathDTO.Edge(getExerciseInEdgeId(competency.getId(), exercise.getId()), startNodeId, getExerciseNodeId(competency.getId(), exercise.getId()))); edges.add(new NgxLearningPathDTO.Edge(getExerciseOutEdgeId(competency.getId(), exercise.getId()), getExerciseNodeId(competency.getId(), exercise.getId()), endNodeId)); }); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java index 761aaf308b12..99bed802d99f 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java @@ -125,7 +125,7 @@ public ResponseEntity getLearningPathId(@PathVariable Long courseId) { @EnforceAtLeastStudent public ResponseEntity getNgxLearningPath(@PathVariable Long learningPathId) { log.debug("REST request to get ngx representation of learning path with id: {}", learningPathId); - LearningPath learningPath = learningPathRepository.findWithEagerCompetenciesAndLearningUnitsByIdElseThrow(learningPathId); + LearningPath learningPath = learningPathRepository.findWithEagerCompetenciesAndLearningObjectsAndCompletedUsersByIdElseThrow(learningPathId); Course course = courseRepository.findByIdElseThrow(learningPath.getCourse().getId()); if (!course.getLearningPathsEnabled()) { throw new BadRequestException("Learning paths are not enabled for this course."); @@ -153,7 +153,7 @@ else if (!authorizationCheckService.isAtLeastInstructorInCourse(course, user) && @EnforceAtLeastStudent public ResponseEntity getRecommendation(@PathVariable Long learningPathId) { log.debug("REST request to get recommendation for learning path with id: {}", learningPathId); - LearningPath learningPath = learningPathRepository.findWithEagerCompetenciesAndLearningUnitsByIdElseThrow(learningPathId); + LearningPath learningPath = learningPathRepository.findWithEagerCompetenciesAndLearningObjectsByIdElseThrow(learningPathId); LearningObject recommendation = learningPathService.getRecommendation(learningPath); if (recommendation == null) { return ResponseEntity.ok(new LearningPathRecommendation(-1, -1, LearningPathRecommendation.RecommendationType.EMPTY)); diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.html b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.html index ca0843db9214..db38a8dd8070 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.html @@ -1,4 +1,7 @@ -
    - - +
    + +
    + + + diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.ts index 23d8ca4149b5..39d2baf34dbe 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.ts @@ -1,24 +1,19 @@ import { Component, Input } from '@angular/core'; -import { faCheckCircle, faCircle, faInfoCircle } from '@fortawesome/free-solid-svg-icons'; +import { faCheckCircle, faCircle, faPlayCircle, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; +import { NgxLearningPathNode, NodeType } from 'app/entities/learning-path.model'; -export enum NodeType { - COMPETENCY_START, - COMPETENCY_END, - MATCH_START, - MATCH_END, - COMPLETED, -} @Component({ selector: 'jhi-learning-path-graph-node', templateUrl: './learning-path-graph-node.component.html', }) export class LearningPathGraphNodeComponent { - @Input() - type: NodeType; + @Input() node: NgxLearningPathNode; //icons faCheckCircle = faCheckCircle; - faInfoCircle = faInfoCircle; + faPlayCircle = faPlayCircle; + faQuestionCircle = faQuestionCircle; + faCircle = faCircle; protected readonly NodeType = NodeType; } diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.html b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.html index 9a8f5baff820..8da15aa6ade3 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.html @@ -21,18 +21,15 @@ - + - + - - text - diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.scss b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.scss index a995d3a4614a..1b2978297de2 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.scss +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.scss @@ -7,4 +7,10 @@ .ngx-graph { width: auto; } + + .node { + display: flex; + width: 100%; + height: 100%; + } } diff --git a/src/main/webapp/app/entities/learning-path.model.ts b/src/main/webapp/app/entities/learning-path.model.ts index b6573c8d0fb2..1208eeca9eba 100644 --- a/src/main/webapp/app/entities/learning-path.model.ts +++ b/src/main/webapp/app/entities/learning-path.model.ts @@ -40,12 +40,12 @@ export class NgxLearningPathCluster implements ClusterNode { public childNodeIds?: string[]; } export enum NodeType { - COMPETENCY_START, - COMPETENCY_END, - MATCH_START, - MATCH_END, - EXERCISE, - LECTURE_UNIT, + COMPETENCY_START = 'COMPETENCY_START', + COMPETENCY_END = 'COMPETENCY_END', + MATCH_START = 'MATCH_START', + MATCH_END = 'MATCH_END', + EXERCISE = 'EXERCISE', + LECTURE_UNIT = 'LECTURE_UNIT', } export class LearningPathRecommendation { diff --git a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java index 09bb6d32cccf..dddedc1d99b3 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java @@ -60,7 +60,7 @@ class LearningPathServiceTest extends AbstractSpringIntegrationBambooBitbucketJi private void generateAndAssert(NgxLearningPathDTO expected) { LearningPath learningPath = learningPathUtilService.createLearningPathInCourse(course); - learningPath = learningPathRepository.findWithEagerCompetenciesAndLearningUnitsByIdElseThrow(learningPath.getId()); + learningPath = learningPathRepository.findWithEagerCompetenciesAndLearningObjectsAndCompletedUsersByIdElseThrow(learningPath.getId()); NgxLearningPathDTO actual = learningPathService.generateNgxRepresentation(learningPath); assertThat(actual).isNotNull(); assertNgxRepEquals(actual, expected); @@ -283,7 +283,7 @@ void setAuthorizationForRepositoryRequests() { void testGetRecommendationEmpty() { competencyUtilService.createCompetency(course); LearningPath learningPath = learningPathUtilService.createLearningPathInCourse(course); - learningPath = learningPathRepository.findWithEagerCompetenciesAndLearningUnitsByIdElseThrow(learningPath.getId()); + learningPath = learningPathRepository.findWithEagerCompetenciesAndLearningObjectsByIdElseThrow(learningPath.getId()); assertThat(learningPathService.getRecommendation(learningPath)).isNull(); } @@ -293,7 +293,7 @@ void testGetRecommendationNotEmpty() { final var lectureUnit = lectureUtilService.createTextUnit(); competencyUtilService.linkLectureUnitToCompetency(competency, lectureUnit); LearningPath learningPath = learningPathUtilService.createLearningPathInCourse(course); - learningPath = learningPathRepository.findWithEagerCompetenciesAndLearningUnitsByIdElseThrow(learningPath.getId()); + learningPath = learningPathRepository.findWithEagerCompetenciesAndLearningObjectsByIdElseThrow(learningPath.getId()); assertThat(learningPathService.getRecommendation(learningPath)).isNotNull(); } } From 8cea81fa05c84476a15dc218385ef93bbc6c637e Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Fri, 21 Jul 2023 02:07:47 +0200 Subject: [PATCH 062/215] added tests and cleanup --- .../learning-path-graph-node.component.html | 6 +-- .../learning-path-graph.component.scss | 1 - .../learning-path-graph.component.ts | 3 +- .../learning-path-management.component.ts | 11 +++- ...earning-path-progress-modal.component.html | 3 ++ ...earning-path-progress-modal.component.scss | 12 +++++ .../learning-path-progress-modal.component.ts | 15 ++++++ .../learning-paths/learning-paths.module.ts | 10 +++- .../learning-path-container.component.ts | 2 - ...arning-path-lecture-unit-view.component.ts | 1 - .../discussion-section.component.ts | 1 - ...learning-path-graph-node.component.spec.ts | 44 ++++++++++++++++ .../learning-path-graph.component.spec.ts | 51 +++++++++++++++++++ ...ning-path-progress-modal.component.spec.ts | 45 ++++++++++++++++ .../learning-path-container.component.spec.ts | 24 +++++++++ ...g-path-lecture-unit-view.component.spec.ts | 33 +++++++++++- 16 files changed, 249 insertions(+), 13 deletions(-) create mode 100644 src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.html create mode 100644 src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.scss create mode 100644 src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.ts create mode 100644 src/test/javascript/spec/component/learning-paths/graph/learning-path-graph-node.component.spec.ts create mode 100644 src/test/javascript/spec/component/learning-paths/graph/learning-path-graph.component.spec.ts create mode 100644 src/test/javascript/spec/component/learning-paths/management/learning-path-progress-modal.component.spec.ts diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.html b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.html index db38a8dd8070..dc85250d2733 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.html @@ -1,7 +1,7 @@
    - - + +
    - + diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.scss b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.scss index 1b2978297de2..0a66e3296ee5 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.scss +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.scss @@ -2,7 +2,6 @@ display: block; width: 100%; height: 100%; - overflow: hidden; .ngx-graph { width: auto; diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts index cc323aced122..860236e0481b 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnChanges, OnInit } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Layout } from '@swimlane/ngx-graph'; import * as shape from 'd3-shape'; @@ -42,7 +42,6 @@ export class LearningPathGraphComponent implements OnInit { this.learningPathService.getNgxLearningPath(this.learningPathId).subscribe((ngxLearningPathResponse) => { this.ngxLearningPath = ngxLearningPathResponse.body!; this.isLoading = false; - console.log(this.ngxLearningPath); }); } diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts index b940cabed71b..ce90a3daac33 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts @@ -13,6 +13,9 @@ import { LearningPathPagingService } from 'app/course/learning-paths/learning-pa import { SortService } from 'app/shared/service/sort.service'; import { LearningPath } from 'app/entities/learning-path.model'; import { faSort } from '@fortawesome/free-solid-svg-icons'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { ExamImportComponent } from 'app/exam/manage/exams/exam-import/exam-import.component'; +import { LearningPathProgressModalComponent } from 'app/course/learning-paths/learning-path-management/learning-path-progress-modal.component'; export enum TableColumn { ID = 'ID', @@ -58,6 +61,7 @@ export class LearningPathManagementComponent implements OnInit, OnDestroy { private alertService: AlertService, private pagingService: LearningPathPagingService, private sortService: SortService, + private modalService: NgbModal, ) {} get page(): number { @@ -202,6 +206,11 @@ export class LearningPathManagementComponent implements OnInit, OnDestroy { } } viewLearningPath(learningPath: LearningPath) { - // TODO + const modalRef = this.modalService.open(LearningPathProgressModalComponent, { + size: 'xl', + backdrop: 'static', + windowClass: 'learning-path-modal', + }); + modalRef.componentInstance.learningPathId = learningPath.id; } } diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.html b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.html new file mode 100644 index 000000000000..5d15f51178bf --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.html @@ -0,0 +1,3 @@ + diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.scss b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.scss new file mode 100644 index 000000000000..813a13004d4a --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.scss @@ -0,0 +1,12 @@ +.modal-container { + display: flex; + height: 90vh; + + .graph { + width: 100%; + } +} + +.learning-path-modal .modal-dialog .modal-content { + min-height: 500px; +} diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.ts new file mode 100644 index 000000000000..478979f37922 --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.ts @@ -0,0 +1,15 @@ +import { Component, Input } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +@Component({ + selector: 'jhi-learning-path-progress-modal', + styleUrls: ['./learning-path-progress-modal.component.scss'], + templateUrl: './learning-path-progress-modal.component.html', +}) +export class LearningPathProgressModalComponent { + @Input() learningPathId: number; + constructor(private activeModal: NgbActiveModal) {} + + close() { + this.activeModal.close(); + } +} diff --git a/src/main/webapp/app/course/learning-paths/learning-paths.module.ts b/src/main/webapp/app/course/learning-paths/learning-paths.module.ts index 103e810f6a2a..7f03e4c6e2f0 100644 --- a/src/main/webapp/app/course/learning-paths/learning-paths.module.ts +++ b/src/main/webapp/app/course/learning-paths/learning-paths.module.ts @@ -12,6 +12,7 @@ import { LearningPathGraphComponent } from 'app/course/learning-paths/learning-p import { LearningPathGraphNodeComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph-node.component'; import { NgxGraphModule } from '@swimlane/ngx-graph'; import { ArtemisLectureUnitsModule } from 'app/overview/course-lectures/lecture-units.module'; +import { LearningPathProgressModalComponent } from 'app/course/learning-paths/learning-path-management/learning-path-progress-modal.component'; const routes: Routes = [ { @@ -54,7 +55,14 @@ const routes: Routes = [ @NgModule({ imports: [ArtemisSharedModule, FormsModule, ReactiveFormsModule, ArtemisSharedComponentModule, NgxGraphModule, RouterModule.forChild(routes), ArtemisLectureUnitsModule], - declarations: [LearningPathContainerComponent, LearningPathManagementComponent, LearningPathGraphSidebarComponent, LearningPathGraphComponent, LearningPathGraphNodeComponent], + declarations: [ + LearningPathContainerComponent, + LearningPathManagementComponent, + LearningPathGraphSidebarComponent, + LearningPathGraphComponent, + LearningPathGraphNodeComponent, + LearningPathProgressModalComponent, + ], exports: [LearningPathContainerComponent], }) export class ArtemisLearningPathsModule {} diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts index 11285c292c05..53793b40c7d5 100644 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts @@ -50,7 +50,6 @@ export class LearningPathContainerComponent implements OnInit { } this.learningPathService.getLearningPathId(this.courseId).subscribe((learningPathIdResponse) => { this.learningPathId = learningPathIdResponse.body!; - console.log('container' + this.learningPathId); }); } @@ -124,7 +123,6 @@ export class LearningPathContainerComponent implements OnInit { * @param instance The component instance */ onChildActivate(instance: LearningPathLectureUnitViewComponent | CourseExerciseDetailsComponent) { - console.log(instance); if (instance instanceof LearningPathLectureUnitViewComponent) { this.setupLectureUnitView(instance); } else { diff --git a/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.ts b/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.ts index 97dee6226034..5ad133a7b589 100644 --- a/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.ts +++ b/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.ts @@ -46,7 +46,6 @@ export class LearningPathLectureUnitViewComponent { * @param instance The component instance */ onChildActivate(instance: DiscussionSectionComponent) { - console.log(instance); this.discussionComponent = instance; // save the reference to the component instance if (this.lecture) { instance.lecture = this.lecture; diff --git a/src/main/webapp/app/overview/discussion-section/discussion-section.component.ts b/src/main/webapp/app/overview/discussion-section/discussion-section.component.ts index 9c544455b565..edc9ed17430f 100644 --- a/src/main/webapp/app/overview/discussion-section/discussion-section.component.ts +++ b/src/main/webapp/app/overview/discussion-section/discussion-section.component.ts @@ -66,7 +66,6 @@ export class DiscussionSectionComponent extends CourseDiscussionDirective implem params: this.activatedRoute.params, queryParams: this.activatedRoute.queryParams, }).subscribe((routeParams: { params: Params; queryParams: Params }) => { - console.log(routeParams.params); this.currentPostId = +routeParams.queryParams.postId; this.course = this.exercise?.course ?? this.lecture?.course; this.metisService.setCourse(this.course); diff --git a/src/test/javascript/spec/component/learning-paths/graph/learning-path-graph-node.component.spec.ts b/src/test/javascript/spec/component/learning-paths/graph/learning-path-graph-node.component.spec.ts new file mode 100644 index 000000000000..844a9462e972 --- /dev/null +++ b/src/test/javascript/spec/component/learning-paths/graph/learning-path-graph-node.component.spec.ts @@ -0,0 +1,44 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ArtemisTestModule } from '../../../test.module'; +import { By } from '@angular/platform-browser'; +import { LearningPathGraphNodeComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph-node.component'; +import { NgxLearningPathNode, NodeType } from 'app/entities/learning-path.model'; + +describe('LearningPathGraphNodeComponent', () => { + let fixture: ComponentFixture; + let comp: LearningPathGraphNodeComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule], + declarations: [LearningPathGraphNodeComponent], + providers: [], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(LearningPathGraphNodeComponent); + comp = fixture.componentInstance; + }); + }); + + it.each([NodeType.EXERCISE, NodeType.LECTURE_UNIT])('should display correct icon for completed learning object', (type: NodeType) => { + comp.node = { id: '1', type: type, completed: true } as NgxLearningPathNode; + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('#completed')).nativeElement).toBeTruthy(); + }); + + it.each([NodeType.EXERCISE, NodeType.LECTURE_UNIT])('should display correct icon for not completed learning object', (type: NodeType) => { + comp.node = { id: '1', type: type, completed: false } as NgxLearningPathNode; + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('#not-completed')).nativeElement).toBeTruthy(); + }); + + it.each([NodeType.COMPETENCY_START, NodeType.COMPETENCY_END, NodeType.COMPETENCY_START, NodeType.COMPETENCY_END])( + 'should display correct icon for generic node', + (type: NodeType) => { + comp.node = { id: '1', type: type } as NgxLearningPathNode; + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('#generic')).nativeElement).toBeTruthy(); + }, + ); +}); diff --git a/src/test/javascript/spec/component/learning-paths/graph/learning-path-graph.component.spec.ts b/src/test/javascript/spec/component/learning-paths/graph/learning-path-graph.component.spec.ts new file mode 100644 index 000000000000..aac3ebb8ad1d --- /dev/null +++ b/src/test/javascript/spec/component/learning-paths/graph/learning-path-graph.component.spec.ts @@ -0,0 +1,51 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ArtemisTestModule } from '../../../test.module'; +import { LearningPathGraphComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph.component'; +import { LearningPathService } from 'app/course/learning-paths/learning-path.service'; + +describe('LearningPathGraphComponent', () => { + let fixture: ComponentFixture; + let comp: LearningPathGraphComponent; + let learningPathService: LearningPathService; + let getNgxLearningPathStub: jest.SpyInstance; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule], + declarations: [LearningPathGraphComponent], + providers: [], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(LearningPathGraphComponent); + comp = fixture.componentInstance; + learningPathService = TestBed.inject(LearningPathService); + getNgxLearningPathStub = jest.spyOn(learningPathService, 'getNgxLearningPath'); + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should load learning path from service', () => { + comp.learningPathId = 1; + fixture.detectChanges(); + expect(getNgxLearningPathStub).toHaveBeenCalledOnce(); + expect(getNgxLearningPathStub).toHaveBeenCalledWith(1); + }); + + it('should update, center, and zoom to fit on resize', () => { + const updateStub = jest.spyOn(comp.update$, 'next'); + const centerStub = jest.spyOn(comp.center$, 'next'); + const zoomToFitStub = jest.spyOn(comp.zoomToFit$, 'next'); + fixture.detectChanges(); + comp.onResize(); + expect(updateStub).toHaveBeenCalledOnce(); + expect(updateStub).toHaveBeenCalledWith(true); + expect(centerStub).toHaveBeenCalledOnce(); + expect(centerStub).toHaveBeenCalledWith(true); + expect(zoomToFitStub).toHaveBeenCalledOnce(); + expect(zoomToFitStub).toHaveBeenCalledWith(true); + }); +}); diff --git a/src/test/javascript/spec/component/learning-paths/management/learning-path-progress-modal.component.spec.ts b/src/test/javascript/spec/component/learning-paths/management/learning-path-progress-modal.component.spec.ts new file mode 100644 index 000000000000..69044986dbba --- /dev/null +++ b/src/test/javascript/spec/component/learning-paths/management/learning-path-progress-modal.component.spec.ts @@ -0,0 +1,45 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ArtemisTestModule } from '../../../test.module'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { MockComponent } from 'ng-mocks'; +import { LearningPathProgressModalComponent } from 'app/course/learning-paths/learning-path-management/learning-path-progress-modal.component'; +import { LearningPathGraphComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph.component'; +import { By } from '@angular/platform-browser'; + +describe('LearningPathProgressModalComponent', () => { + let fixture: ComponentFixture; + let comp: LearningPathProgressModalComponent; + let activeModal: NgbActiveModal; + let closeStub: jest.SpyInstance; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule, MockComponent(LearningPathGraphComponent)], + declarations: [LearningPathProgressModalComponent], + providers: [], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(LearningPathProgressModalComponent); + comp = fixture.componentInstance; + activeModal = TestBed.inject(NgbActiveModal); + closeStub = jest.spyOn(activeModal, 'close'); + fixture.detectChanges(); + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should display learning path graph if id is present', () => { + comp.learningPathId = 1; + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('.graph')).nativeElement).toBeTruthy(); + }); + + it('should correctly close modal', () => { + comp.close(); + expect(closeStub).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/test/javascript/spec/component/learning-paths/participate/learning-path-container.component.spec.ts b/src/test/javascript/spec/component/learning-paths/participate/learning-path-container.component.spec.ts index 860931670aa3..a63cd02a0a3f 100644 --- a/src/test/javascript/spec/component/learning-paths/participate/learning-path-container.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/participate/learning-path-container.component.spec.ts @@ -16,6 +16,8 @@ import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service' import { LearningPathGraphSidebarComponent } from 'app/course/learning-paths/participate/learning-path-graph-sidebar.component'; import { AttachmentUnit } from 'app/entities/lecture-unit/attachmentUnit.model'; import { TextExercise } from 'app/entities/text-exercise.model'; +import { LearningPathLectureUnitViewComponent } from 'app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component'; +import { CourseExerciseDetailsComponent } from 'app/overview/exercise-details/course-exercise-details.component'; describe('LearningPathContainerComponent', () => { let fixture: ComponentFixture; @@ -156,4 +158,26 @@ describe('LearningPathContainerComponent', () => { expect(getExerciseDetailsStub).toHaveBeenCalled(); expect(getExerciseDetailsStub).toHaveBeenCalledWith(exercise.id); }); + + it('should set properties of lecture unit view on activate', () => { + comp.learningObjectId = lectureUnit.id!; + comp.lectureUnit = lectureUnit; + comp.lectureId = lecture.id; + comp.lecture = lecture; + fixture.detectChanges(); + const instance = { lecture: undefined, lectureUnit: undefined } as unknown as LearningPathLectureUnitViewComponent; + comp.setupLectureUnitView(instance); + expect(instance.lecture).toEqual(lecture); + expect(instance.lectureUnit).toEqual(lectureUnit); + }); + + it('should set properties of exercise view on activate', () => { + comp.exercise = exercise; + comp.learningObjectId = exercise.id!; + fixture.detectChanges(); + const instance = { courseId: undefined, exerciseId: undefined } as unknown as CourseExerciseDetailsComponent; + comp.setupExerciseView(instance); + expect(instance.courseId).toBe(1); + expect(instance.exerciseId).toEqual(exercise.id); + }); }); diff --git a/src/test/javascript/spec/component/learning-paths/participate/learning-path-lecture-unit-view.component.spec.ts b/src/test/javascript/spec/component/learning-paths/participate/learning-path-lecture-unit-view.component.spec.ts index edf58948cd6b..137578892fef 100644 --- a/src/test/javascript/spec/component/learning-paths/participate/learning-path-lecture-unit-view.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/participate/learning-path-lecture-unit-view.component.spec.ts @@ -3,7 +3,7 @@ import { MockModule } from 'ng-mocks'; import { ArtemisTestModule } from '../../../test.module'; import { RouterModule } from '@angular/router'; import { Lecture } from 'app/entities/lecture.model'; -import { LearningPathLectureUnitViewComponent } from 'app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component'; +import { LearningPathLectureUnitViewComponent, LectureUnitCompletionEvent } from 'app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component'; import { AttachmentUnit } from 'app/entities/lecture-unit/attachmentUnit.model'; import { ArtemisLectureUnitsModule } from 'app/overview/course-lectures/lecture-units.module'; import { LectureUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/lectureUnit.service'; @@ -11,6 +11,7 @@ import { VideoUnit } from 'app/entities/lecture-unit/videoUnit.model'; import { TextUnit } from 'app/entities/lecture-unit/textUnit.model'; import { OnlineUnit } from 'app/entities/lecture-unit/onlineUnit.model'; import { Course, CourseInformationSharingConfiguration } from 'app/entities/course.model'; +import { DiscussionSectionComponent } from 'app/overview/discussion-section/discussion-section.component'; describe('LearningPathLectureUnitViewComponent', () => { let fixture: ComponentFixture; @@ -108,4 +109,34 @@ describe('LearningPathLectureUnitViewComponent', () => { const outlet = fixture.debugElement.nativeElement.querySelector('router-outlet'); expect(outlet).toBeTruthy(); }); + + it('should set lecture unit completion', () => { + const attachment = new AttachmentUnit(); + attachment.id = 3; + attachment.visibleToStudents = true; + attachment.completed = false; + lecture.lectureUnits = [attachment]; + comp.lecture = lecture; + comp.lectureUnit = attachment; + lecture.course = new Course(); + fixture.detectChanges(); + const event = { lectureUnit: attachment, completed: true } as LectureUnitCompletionEvent; + comp.completeLectureUnit(event); + expect(setCompletionStub).toHaveBeenCalledOnce(); + expect(setCompletionStub).toHaveBeenCalledWith(attachment.id, lecture.id, event.completed); + }); + + it('should set properties of child on activate', () => { + const attachment = new AttachmentUnit(); + attachment.id = 3; + lecture.lectureUnits = [attachment]; + comp.lecture = lecture; + comp.lectureUnit = attachment; + lecture.course = new Course(); + fixture.detectChanges(); + const instance = { lecture: undefined, isCommunicationPage: undefined } as DiscussionSectionComponent; + comp.onChildActivate(instance); + expect(instance.lecture).toEqual(lecture); + expect(instance.isCommunicationPage).toBeFalsy(); + }); }); From 21ce9a9fd3767858de28e6e3f20cb1ccf3f5fb15 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Fri, 21 Jul 2023 10:25:19 +0200 Subject: [PATCH 063/215] fix codacy warnings --- .../learning-path-management.component.ts | 1 - .../artemis/competency/CompetencyUtilService.java | 9 ++++++++- .../competency/LearningPathUtilService.java | 14 +++++++------- .../www1/artemis/exercise/ExerciseUtilService.java | 7 ------- .../lecture/LearningPathIntegrationTest.java | 13 ++++--------- .../artemis/service/LearningPathServiceTest.java | 4 ++-- 6 files changed, 21 insertions(+), 27 deletions(-) diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts index ce90a3daac33..46526a35925b 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts @@ -14,7 +14,6 @@ import { SortService } from 'app/shared/service/sort.service'; import { LearningPath } from 'app/entities/learning-path.model'; import { faSort } from '@fortawesome/free-solid-svg-icons'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { ExamImportComponent } from 'app/exam/manage/exams/exam-import/exam-import.component'; import { LearningPathProgressModalComponent } from 'app/course/learning-paths/learning-path-management/learning-path-progress-modal.component'; export enum TableColumn { diff --git a/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java b/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java index 6451d226004a..ab93c366b286 100644 --- a/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java @@ -63,7 +63,7 @@ public Competency createCompetency(Course course) { public Competency[] createCompetencies(Course course, int numberOfCompetencies) { Competency[] competencies = new Competency[numberOfCompetencies]; for (int i = 0; i < competencies.length; i++) { - competencies[i] = createCompetency(course, "" + i); + competencies[i] = createCompetency(course, String.valueOf(i)); } return competencies; } @@ -90,6 +90,13 @@ public void linkExerciseToCompetency(Competency competency, Exercise exercise) { exerciseRepository.save(exercise); } + /** + * Adds a relation between competencies. + * + * @param tail the competency that relates to another competency + * @param type the type of the relation + * @param head the competency that the tail competency relates to + */ public void addRelation(Competency tail, CompetencyRelation.RelationType type, Competency head) { CompetencyRelation relation = new CompetencyRelation(); relation.setTailCompetency(tail); diff --git a/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java b/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java index 658848ba26c2..7e49d00c7bb0 100644 --- a/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java @@ -38,10 +38,10 @@ public class LearningPathUtilService { * @return the updated course */ public Course enableAndGenerateLearningPathsForCourse(Course course) { - course = courseRepository.findWithEagerLearningPathsAndCompetenciesByIdElseThrow(course.getId()); - learningPathService.generateLearningPaths(course); - course.setLeanringPathsEnabled(true); - return courseRepository.save(course); + var eagerlyLoadedCourse = courseRepository.findWithEagerLearningPathsAndCompetenciesByIdElseThrow(course.getId()); + learningPathService.generateLearningPaths(eagerlyLoadedCourse); + eagerlyLoadedCourse.setLeanringPathsEnabled(true); + return courseRepository.save(eagerlyLoadedCourse); } public LearningPath createLearningPathInCourse(Course course) { @@ -52,8 +52,8 @@ public LearningPath createLearningPathInCourse(Course course) { } public LearningPath createLearningPath(Set competencies) { - LearningPath lp = new LearningPath(); - lp.setCompetencies(competencies); - return learningPathRepository.save(lp); + LearningPath learningPath = new LearningPath(); + learningPath.setCompetencies(competencies); + return learningPathRepository.save(learningPath); } } diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/ExerciseUtilService.java b/src/test/java/de/tum/in/www1/artemis/exercise/ExerciseUtilService.java index 56bd065e38d4..7f986ecb7241 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/ExerciseUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/ExerciseUtilService.java @@ -12,7 +12,6 @@ import org.springframework.stereotype.Service; import de.tum.in.www1.artemis.domain.*; -import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; import de.tum.in.www1.artemis.domain.enumeration.IncludedInOverallScore; import de.tum.in.www1.artemis.domain.exam.Exam; @@ -314,12 +313,6 @@ public ProgrammingExercise findProgrammingExerciseWithTitle(Collection return new ProgrammingExercise(); } - public Exercise addCompetenciesToExercise(Exercise exercise, Set competencies) { - exercise = exerciseRepo.findByIdElseThrow(exercise.getId()); - exercise.setCompetencies(competencies); - return exerciseRepo.save(exercise); - } - public Channel addChannelToExercise(Exercise exercise) { Channel channel = ConversationFactory.generateChannel(exercise.getCourseViaExerciseGroupOrCourseMember()); channel.setExercise(exercise); diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java index 49e9590407cd..646ed5301c62 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java @@ -95,13 +95,13 @@ class LearningPathIntegrationTest extends AbstractSpringIntegrationBambooBitbuck private final int NUMBER_OF_STUDENTS = 5; - private final String STUDENT_OF_COURSE = TEST_PREFIX + "student1"; + private static final String STUDENT_OF_COURSE = TEST_PREFIX + "student1"; - private final String TUTOR_OF_COURSE = TEST_PREFIX + "tutor1"; + private static final String TUTOR_OF_COURSE = TEST_PREFIX + "tutor1"; - private final String EDITOR_OF_COURSE = TEST_PREFIX + "editor1"; + private static final String EDITOR_OF_COURSE = TEST_PREFIX + "editor1"; - private final String INSTRUCTOR_OF_COURSE = TEST_PREFIX + "instructor1"; + private static final String INSTRUCTOR_OF_COURSE = TEST_PREFIX + "instructor1"; private User studentNotInCourse; @@ -169,11 +169,6 @@ private Competency importCompetencyRESTCall() throws Exception { return request.postWithResponseBody("/api/courses/" + course.getId() + "/competencies/import", competencyToImport, Competency.class, HttpStatus.CREATED); } - private Competency updateCompetencyRESTCall() throws Exception { - competencies[0].setTitle("Updated Title"); - return request.putWithResponseBody("/api/courses/" + course.getId() + "/competencies", competencies[0], Competency.class, HttpStatus.OK); - } - private void deleteCompetencyRESTCall(Competency competency) throws Exception { request.delete("/api/courses/" + course.getId() + "/competencies/" + competency.getId(), HttpStatus.OK); } diff --git a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java index dddedc1d99b3..1ccb42a0df64 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java @@ -113,7 +113,7 @@ void testCompetencyWithLectureUnitAndExercise() { competencyUtilService.linkExerciseToCompetency(competency, exercise); final var startNodeId = LearningPathService.getCompetencyStartNodeId(competency.getId()); final var endNodeId = LearningPathService.getCompetencyEndNodeId(competency.getId()); - HashSet expectedNodes = getExpectedNodesOfEmptyCompetency(competency); + Set expectedNodes = getExpectedNodesOfEmptyCompetency(competency); expectedNodes.add(getNodeForLectureUnit(competency, lectureUnit)); expectedNodes.add(getNodeForExercise(competency, exercise)); Set expectedEdges = Set.of( @@ -255,7 +255,7 @@ private void addExpectedComponentsForEmptyCompetencies(Competency... competencie } } - private static HashSet getExpectedNodesOfEmptyCompetency(Competency competency) { + private static Set getExpectedNodesOfEmptyCompetency(Competency competency) { return new HashSet<>(Set.of( new NgxLearningPathDTO.Node(LearningPathService.getCompetencyStartNodeId(competency.getId()), NgxLearningPathDTO.NodeType.COMPETENCY_START, competency.getId(), ""), new NgxLearningPathDTO.Node(LearningPathService.getCompetencyEndNodeId(competency.getId()), NgxLearningPathDTO.NodeType.COMPETENCY_END, competency.getId(), ""))); From f8daead37d1e477d13432f377eb275558c9291fd Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Fri, 21 Jul 2023 13:18:20 +0200 Subject: [PATCH 064/215] Fix more codestyle issues --- .../artemis/service/LearningPathService.java | 31 +++++++++--- .../artemis/web/rest/CompetencyResource.java | 5 +- .../web/rest/LearningPathResource.java | 4 +- .../dto/learningpath/NgxLearningPathDTO.java | 21 ++++++++ ...learning-path-graph-sidebar.component.scss | 10 ---- .../course-learning-path.component.html | 1 - .../course-learning-path.component.ts | 20 -------- .../course-learning-path.module.ts | 28 ----------- .../app/overview/courses-routing.module.ts | 2 +- .../competency/CompetencyUtilService.java | 5 +- .../competency/LearningPathUtilService.java | 12 +++++ .../lecture/LearningPathIntegrationTest.java | 50 +++++++++++-------- 12 files changed, 99 insertions(+), 90 deletions(-) delete mode 100644 src/main/webapp/app/overview/course-learning-path/course-learning-path.component.html delete mode 100644 src/main/webapp/app/overview/course-learning-path/course-learning-path.component.ts delete mode 100644 src/main/webapp/app/overview/course-learning-path/course-learning-path.module.ts diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index 9b1c5509f479..daccbbf8a581 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -1,6 +1,9 @@ package de.tum.in.www1.artemis.service; -import java.util.*; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import java.util.stream.LongStream; @@ -155,10 +158,14 @@ private void updateLearningPathProgress(final LearningPath learningPath) { final var competencyIds = learningPath.getCompetencies().stream().map(Competency::getId).collect(Collectors.toSet()); final var competencyProgresses = competencyProgressRepository.findAllByCompetencyIdsAndUserId(competencyIds, userId); - // TODO: consider optional competencies final var completed = (float) competencyProgresses.stream().filter(CompetencyProgressService::isMastered).count(); final var numberOfCompetencies = learningPath.getCompetencies().size(); - learningPath.setProgress(numberOfCompetencies == 0 ? 0 : Math.round(completed * 100 / (float) numberOfCompetencies)); + if (numberOfCompetencies == 0) { + learningPath.setProgress(0); + } + else { + learningPath.setProgress(Math.round(completed * 100 / (float) numberOfCompetencies)); + } learningPathRepository.save(learningPath); log.debug("Updated LearningPath (id={}) for user (id={})", learningPath.getId(), userId); } @@ -312,11 +319,21 @@ private void generateNgxRepresentationForRelations(LearningPath learningPath, Se private void generateNgxRepresentationForRelation(CompetencyRelation relation, Map competencyToMatchCluster, Set createdRelations, Set edges) { final var sourceId = relation.getHeadCompetency().getId(); - final String sourceNodeId = competencyToMatchCluster.containsKey(sourceId) ? getMatchingClusterEndNodeId(competencyToMatchCluster.get(sourceId)) - : getCompetencyEndNodeId(sourceId); + String sourceNodeId; + if (competencyToMatchCluster.containsKey(sourceId)) { + sourceNodeId = getMatchingClusterEndNodeId(competencyToMatchCluster.get(sourceId)); + } + else { + sourceNodeId = getCompetencyEndNodeId(sourceId); + } final var targetId = relation.getTailCompetency().getId(); - final String targetNodeId = competencyToMatchCluster.containsKey(targetId) ? getMatchingClusterStartNodeId(competencyToMatchCluster.get(targetId)) - : getCompetencyStartNodeId(targetId); + String targetNodeId; + if (competencyToMatchCluster.containsKey(targetId)) { + targetNodeId = getMatchingClusterStartNodeId(competencyToMatchCluster.get(targetId)); + } + else { + targetNodeId = getCompetencyStartNodeId(targetId); + } final String relationEdgeId = getRelationEdgeId(sourceNodeId, targetNodeId); // skip if relation has already been created (possible for edges linked to matching cluster start/end nodes) if (!createdRelations.contains(relationEdgeId)) { diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java index 27fc1cf75152..78cdff7931b4 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java @@ -25,7 +25,10 @@ import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastEditor; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent; -import de.tum.in.www1.artemis.service.*; +import de.tum.in.www1.artemis.service.AuthorizationCheckService; +import de.tum.in.www1.artemis.service.CompetencyProgressService; +import de.tum.in.www1.artemis.service.CompetencyService; +import de.tum.in.www1.artemis.service.LearningPathService; import de.tum.in.www1.artemis.service.util.RoundingUtil; import de.tum.in.www1.artemis.web.rest.dto.CourseCompetencyProgressDTO; import de.tum.in.www1.artemis.web.rest.dto.PageableSearchDTO; diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java index 99bed802d99f..3c0968ee3519 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java @@ -12,7 +12,9 @@ import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.competency.LearningPath; import de.tum.in.www1.artemis.domain.lecture.LectureUnit; -import de.tum.in.www1.artemis.repository.*; +import de.tum.in.www1.artemis.repository.CourseRepository; +import de.tum.in.www1.artemis.repository.LearningPathRepository; +import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.security.Role; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent; diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/NgxLearningPathDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/NgxLearningPathDTO.java index 90ca65a77b43..9767380451be 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/NgxLearningPathDTO.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/NgxLearningPathDTO.java @@ -1,5 +1,6 @@ package de.tum.in.www1.artemis.web.rest.dto.learningpath; +import java.util.Objects; import java.util.Set; import com.fasterxml.jackson.annotation.JsonInclude; @@ -10,6 +11,11 @@ @JsonInclude(JsonInclude.Include.NON_EMPTY) public record NgxLearningPathDTO(Set nodes, Set edges, Set clusters) { + @Override + public int hashCode() { + return Objects.hash(nodes, edges, clusters); + } + @Override public boolean equals(Object obj) { if (!(obj instanceof NgxLearningPathDTO other)) { @@ -41,6 +47,11 @@ public Node(String id, NodeType type) { this(id, type, -1); } + @Override + public int hashCode() { + return Objects.hashCode(id); + } + @Override public boolean equals(Object obj) { if (!(obj instanceof Node other)) { @@ -57,6 +68,11 @@ public String toString() { public record Edge(String id, String source, String target) { + @Override + public int hashCode() { + return Objects.hashCode(id); + } + @Override public boolean equals(Object obj) { if (!(obj instanceof Edge other)) { @@ -73,6 +89,11 @@ public String toString() { public record Cluster(String id, String label, Set childNodeIds) { + @Override + public int hashCode() { + return Objects.hashCode(id); + } + @Override public boolean equals(Object obj) { if (!(obj instanceof Cluster other)) { diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.scss b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.scss index 38d1584c12c5..ee0763fe5218 100644 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.scss +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.scss @@ -10,16 +10,6 @@ $graph-min-width: 215px; min-height: 500px; margin-left: auto; - .scrollbar { - position: relative; - max-height: 700px; - overflow: auto; - } - - .wrapper-scroll-y { - display: block; - } - .draggable-right { display: flex; flex-direction: column; diff --git a/src/main/webapp/app/overview/course-learning-path/course-learning-path.component.html b/src/main/webapp/app/overview/course-learning-path/course-learning-path.component.html deleted file mode 100644 index 4cd61a1c7b63..000000000000 --- a/src/main/webapp/app/overview/course-learning-path/course-learning-path.component.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/main/webapp/app/overview/course-learning-path/course-learning-path.component.ts b/src/main/webapp/app/overview/course-learning-path/course-learning-path.component.ts deleted file mode 100644 index c9d342f129c1..000000000000 --- a/src/main/webapp/app/overview/course-learning-path/course-learning-path.component.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Component, Input, OnInit } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; - -@Component({ - selector: 'jhi-course-learning-path', - templateUrl: './course-learning-path.component.html', - styleUrls: ['../course-overview.scss'], -}) -export class CourseLearningPathComponent implements OnInit { - @Input() - courseId: number; - - constructor(private activatedRoute: ActivatedRoute) {} - - ngOnInit(): void { - this.activatedRoute.parent!.parent!.params.subscribe((params) => { - this.courseId = +params['courseId']; - }); - } -} diff --git a/src/main/webapp/app/overview/course-learning-path/course-learning-path.module.ts b/src/main/webapp/app/overview/course-learning-path/course-learning-path.module.ts deleted file mode 100644 index 538538611bcf..000000000000 --- a/src/main/webapp/app/overview/course-learning-path/course-learning-path.module.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ArtemisSharedModule } from 'app/shared/shared.module'; -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; -import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; -import { Authority } from 'app/shared/constants/authority.constants'; -import { UserRouteAccessService } from 'app/core/auth/user-route-access-service'; -import { ArtemisLearningPathsModule } from 'app/course/learning-paths/learning-paths.module'; -import { CourseLearningPathComponent } from 'app/overview/course-learning-path/course-learning-path.component'; - -const routes: Routes = [ - { - path: '', - pathMatch: 'full', - data: { - authorities: [Authority.USER], - pageTitle: 'overview.learningPath', - }, - component: CourseLearningPathComponent, - canActivate: [UserRouteAccessService], - }, -]; - -@NgModule({ - imports: [RouterModule.forChild(routes), ArtemisSharedModule, ArtemisSharedComponentModule, ArtemisLearningPathsModule], - declarations: [CourseLearningPathComponent], - exports: [CourseLearningPathComponent], -}) -export class CourseLearningPathModule {} diff --git a/src/main/webapp/app/overview/courses-routing.module.ts b/src/main/webapp/app/overview/courses-routing.module.ts index 8fed271b0a52..41cb383835a2 100644 --- a/src/main/webapp/app/overview/courses-routing.module.ts +++ b/src/main/webapp/app/overview/courses-routing.module.ts @@ -67,7 +67,7 @@ const routes: Routes = [ }, { path: 'learning-path', - loadChildren: () => import('./course-learning-path/course-learning-path.module').then((m) => m.CourseLearningPathModule), + loadChildren: () => import('app/course/learning-paths/learning-paths.module').then((m) => m.ArtemisLearningPathsModule), }, { path: 'discussion', diff --git a/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java b/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java index ab93c366b286..f487324867b7 100644 --- a/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java @@ -8,7 +8,10 @@ import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.competency.CompetencyRelation; import de.tum.in.www1.artemis.domain.lecture.LectureUnit; -import de.tum.in.www1.artemis.repository.*; +import de.tum.in.www1.artemis.repository.CompetencyRelationRepository; +import de.tum.in.www1.artemis.repository.CompetencyRepository; +import de.tum.in.www1.artemis.repository.ExerciseRepository; +import de.tum.in.www1.artemis.repository.LectureUnitRepository; /** * Service responsible for initializing the database with specific testdata related to competencies for use in integration tests. diff --git a/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java b/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java index 7e49d00c7bb0..ed19e2442d81 100644 --- a/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java @@ -44,6 +44,12 @@ public Course enableAndGenerateLearningPathsForCourse(Course course) { return courseRepository.save(eagerlyLoadedCourse); } + /** + * Creates learning path for course. + * + * @param course the course for which the learning path should be generated + * @return the persisted learning path + */ public LearningPath createLearningPathInCourse(Course course) { final var competencies = competencyRepository.findAllForCourse(course.getId()); LearningPath learningPath = createLearningPath(competencies); @@ -51,6 +57,12 @@ public LearningPath createLearningPathInCourse(Course course) { return learningPathRepository.save(learningPath); } + /** + * Creates learning path. + * + * @param competencies the competencies that will be linked to the learning path + * @return the persisted learning path + */ public LearningPath createLearningPath(Set competencies) { LearningPath learningPath = new LearningPath(); learningPath.setCompetencies(competencies); diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java index 646ed5301c62..211ce23947c4 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java @@ -6,9 +6,14 @@ import java.time.ZonedDateTime; import java.util.List; import java.util.Set; +import java.util.function.Function; +import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; @@ -151,8 +156,8 @@ private void testAllPreAuthorize() throws Exception { request.getSearchResult("/api/courses/" + course.getId() + "/learning-paths", HttpStatus.FORBIDDEN, LearningPath.class, pageableSearchUtilService.searchMapping(search)); } - private Course enableLearningPathsRESTCall(Course course) throws Exception { - return request.putWithResponseBody("/api/courses/" + course.getId() + "/learning-paths/enable", course, Course.class, HttpStatus.OK); + private void enableLearningPathsRESTCall(Course course) throws Exception { + request.putWithResponseBody("/api/courses/" + course.getId() + "/learning-paths/enable", course, Course.class, HttpStatus.OK); } private Competency createCompetencyRESTCall() throws Exception { @@ -275,33 +280,38 @@ void testGetLearningPathsOnPageForCourseExactlyStudent() throws Exception { assertThat(result.getResultsOnPage()).hasSize(1); } - @Test - @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") - void testAddCompetencyToLearningPathsOnCreateCompetency() throws Exception { - course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); - - final var createdCompetency = createCompetencyRESTCall(); - - final var student = userRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); - final var learningPathOptional = learningPathRepository.findWithEagerCompetenciesByCourseIdAndUserId(course.getId(), student.getId()); - assertThat(learningPathOptional).isPresent(); - assertThat(learningPathOptional.get().getCompetencies()).as("should contain new competency").contains(createdCompetency); - assertThat(learningPathOptional.get().getCompetencies().size()).as("should not remove old competencies").isEqualTo(competencies.length + 1); - final var oldCompetencies = Set.of(competencies[0], competencies[1], competencies[2], competencies[3], competencies[4]); - assertThat(learningPathOptional.get().getCompetencies()).as("should not remove old competencies").containsAll(oldCompetencies); + private static Stream addCompetencyToLearningPathsOnCreateAndImportCompetencyTestProvider() { + final Function createCall = (reference) -> { + try { + return reference.createCompetencyRESTCall(); + } + catch (Exception e) { + throw new RuntimeException(e); + } + }; + final Function importCall = (reference) -> { + try { + return reference.importCompetencyRESTCall(); + } + catch (Exception e) { + throw new RuntimeException(e); + } + }; + return Stream.of(Arguments.of(createCall), Arguments.of(importCall)); } - @Test + @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") - void testAddCompetencyToLearningPathsOnImportCompetency() throws Exception { + @MethodSource("addCompetencyToLearningPathsOnCreateAndImportCompetencyTestProvider") + void addCompetencyToLearningPaths(Function restCall) { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); - final var importedCompetency = importCompetencyRESTCall(); + final var newCompetency = restCall.apply(this); final var student = userRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); final var learningPathOptional = learningPathRepository.findWithEagerCompetenciesByCourseIdAndUserId(course.getId(), student.getId()); assertThat(learningPathOptional).isPresent(); - assertThat(learningPathOptional.get().getCompetencies()).as("should contain new competency").contains(importedCompetency); + assertThat(learningPathOptional.get().getCompetencies()).as("should contain new competency").contains(newCompetency); assertThat(learningPathOptional.get().getCompetencies().size()).as("should not remove old competencies").isEqualTo(competencies.length + 1); final var oldCompetencies = Set.of(competencies[0], competencies[1], competencies[2], competencies[3], competencies[4]); assertThat(learningPathOptional.get().getCompetencies()).as("should not remove old competencies").containsAll(oldCompetencies); From 4c298cf8148c11121b20bf1ca7e314fd06ff7818 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Fri, 21 Jul 2023 13:55:26 +0200 Subject: [PATCH 065/215] Fix icon after merge --- src/main/webapp/app/overview/course-overview.component.html | 4 ++-- src/main/webapp/app/overview/course-overview.component.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/app/overview/course-overview.component.html b/src/main/webapp/app/overview/course-overview.component.html index 028058227b1c..82e45fdbe442 100644 --- a/src/main/webapp/app/overview/course-overview.component.html +++ b/src/main/webapp/app/overview/course-overview.component.html @@ -20,8 +20,8 @@ Competencies
    - - Learning Path + + Learning Path diff --git a/src/main/webapp/app/overview/course-overview.component.ts b/src/main/webapp/app/overview/course-overview.component.ts index 835ac782f5b4..b1a8a54fb56c 100644 --- a/src/main/webapp/app/overview/course-overview.component.ts +++ b/src/main/webapp/app/overview/course-overview.component.ts @@ -23,6 +23,7 @@ import { faFlag, faGraduationCap, faListAlt, + faNetworkWired, faPersonChalkboard, faSync, faTable, @@ -82,6 +83,7 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit faWrench = faWrench; faTable = faTable; faFlag = faFlag; + faNetworkWired = faNetworkWired; faListAlt = faListAlt; faChartBar = faChartBar; faFilePdf = faFilePdf; From 7cbf8cb0fc43c13136cff74979c390e118ab27c3 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Sat, 22 Jul 2023 12:50:38 +0200 Subject: [PATCH 066/215] Apply most of Johannes suggestions --- .../de/tum/in/www1/artemis/domain/Course.java | 2 +- .../artemis/service/LearningPathService.java | 41 +++++------ .../web/rest/LearningPathResource.java | 19 ++--- .../dto/learningpath/NgxLearningPathDTO.java | 73 ------------------- .../resources/config/liquibase/master.xml | 2 +- .../competency/LearningPathUtilService.java | 2 +- .../lecture/LearningPathIntegrationTest.java | 4 +- 7 files changed, 32 insertions(+), 111 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Course.java b/src/main/java/de/tum/in/www1/artemis/domain/Course.java index 5fc1c954150b..be0f1b9cbfc7 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Course.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Course.java @@ -733,7 +733,7 @@ public boolean getLearningPathsEnabled() { return learningPathsEnabled; } - public void setLeanringPathsEnabled(boolean learningPathsEnabled) { + public void setLearningPathsEnabled(boolean learningPathsEnabled) { this.learningPathsEnabled = learningPathsEnabled; } diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index daccbbf8a581..3fd0ec8dc887 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -79,15 +79,17 @@ public void generateLearningPaths(@NotNull Course course) { */ public void generateLearningPathForUser(@NotNull Course course, @NotNull User user) { var existingLearningPath = learningPathRepository.findByCourseIdAndUserId(course.getId(), user.getId()); - if (existingLearningPath.isEmpty()) { - LearningPath lpToCreate = new LearningPath(); - lpToCreate.setUser(user); - lpToCreate.setCourse(course); - lpToCreate.getCompetencies().addAll(course.getCompetencies()); - var persistedLearningPath = learningPathRepository.save(lpToCreate); - log.debug("Created LearningPath (id={}) for user (id={}) in course (id={})", persistedLearningPath.getId(), user.getId(), course.getId()); - updateLearningPathProgress(persistedLearningPath); + // the learning path has not to be generated if it already exits + if (existingLearningPath.isPresent()) { + return; } + LearningPath lpToCreate = new LearningPath(); + lpToCreate.setUser(user); + lpToCreate.setCourse(course); + lpToCreate.getCompetencies().addAll(course.getCompetencies()); + var persistedLearningPath = learningPathRepository.save(lpToCreate); + log.debug("Created LearningPath (id={}) for user (id={}) in course (id={})", persistedLearningPath.getId(), user.getId(), course.getId()); + updateLearningPathProgress(persistedLearningPath); } /** @@ -97,7 +99,7 @@ public void generateLearningPathForUser(@NotNull Course course, @NotNull User us * @param course the course the learning paths are linked to * @return A wrapper object containing a list of all found learning paths and the total number of pages */ - public SearchResultPageDTO getAllOfCourseOnPageWithSize(final PageableSearchDTO search, final Course course) { + public SearchResultPageDTO getAllOfCourseOnPageWithSize(@NotNull PageableSearchDTO search, @NotNull Course course) { final var pageable = PageUtil.createLearningPathPageRequest(search); final var searchTerm = search.getSearchTerm(); final Page learningPathPage = learningPathRepository.findByLoginOrNameInCourse(searchTerm, course.getId(), pageable); @@ -110,7 +112,7 @@ public SearchResultPageDTO getAllOfCourseOnPageWithSize(final Page * @param competency Competency that should be added to each learning path * @param courseId course id that the learning paths belong to */ - public void linkCompetencyToLearningPathsOfCourse(Competency competency, long courseId) { + public void linkCompetencyToLearningPathsOfCourse(@NotNull Competency competency, long courseId) { var course = courseRepository.findWithEagerLearningPathsAndCompetenciesByIdElseThrow(courseId); var learningPaths = course.getLearningPaths(); learningPaths.forEach(learningPath -> learningPath.addCompetency(competency)); @@ -124,7 +126,7 @@ public void linkCompetencyToLearningPathsOfCourse(Competency competency, long co * @param competency Competency that should be removed from each learning path * @param courseId course id that the learning paths belong to */ - public void removeLinkedCompetencyFromLearningPathsOfCourse(Competency competency, long courseId) { + public void removeLinkedCompetencyFromLearningPathsOfCourse(@NotNull Competency competency, long courseId) { var course = courseRepository.findWithEagerLearningPathsAndCompetenciesByIdElseThrow(courseId); var learningPaths = course.getLearningPaths(); learningPaths.forEach(learningPath -> learningPath.removeCompetency(competency)); @@ -132,18 +134,13 @@ public void removeLinkedCompetencyFromLearningPathsOfCourse(Competency competenc log.debug("Removed linked competency (id={}) from learning paths", competency.getId()); } - public void updateLearningPathProgress(final long learningPathId) { - final var learningPath = learningPathRepository.findWithEagerCompetenciesByIdElseThrow(learningPathId); - this.updateLearningPathProgress(learningPath); - } - /** * Updates progress of the learning path specified by course and user id. * * @param courseId id of the course the learning path is linked to * @param userId id of the user the learning path is linked to */ - public void updateLearningPathProgress(final long courseId, final long userId) { + public void updateLearningPathProgress(long courseId, long userId) { final var learningPath = learningPathRepository.findWithEagerCompetenciesByCourseIdAndUserId(courseId, userId); learningPath.ifPresent(this::updateLearningPathProgress); } @@ -153,18 +150,18 @@ public void updateLearningPathProgress(final long courseId, final long userId) { * * @param learningPath learning path that is updated */ - private void updateLearningPathProgress(final LearningPath learningPath) { + private void updateLearningPathProgress(@NotNull LearningPath learningPath) { final var userId = learningPath.getUser().getId(); final var competencyIds = learningPath.getCompetencies().stream().map(Competency::getId).collect(Collectors.toSet()); final var competencyProgresses = competencyProgressRepository.findAllByCompetencyIdsAndUserId(competencyIds, userId); - final var completed = (float) competencyProgresses.stream().filter(CompetencyProgressService::isMastered).count(); + final float completed = competencyProgresses.stream().filter(CompetencyProgressService::isMastered).count(); final var numberOfCompetencies = learningPath.getCompetencies().size(); if (numberOfCompetencies == 0) { learningPath.setProgress(0); } else { - learningPath.setProgress(Math.round(completed * 100 / (float) numberOfCompetencies)); + learningPath.setProgress(Math.round(completed * 100 / numberOfCompetencies)); } learningPathRepository.save(learningPath); log.debug("Updated LearningPath (id={}) for user (id={})", learningPath.getId(), userId); @@ -177,7 +174,7 @@ private void updateLearningPathProgress(final LearningPath learningPath) { * @return Ngx representation of the learning path * @see NgxLearningPathDTO */ - public NgxLearningPathDTO generateNgxRepresentation(LearningPath learningPath) { + public NgxLearningPathDTO generateNgxRepresentation(@NotNull LearningPath learningPath) { Set nodes = new HashSet<>(); Set edges = new HashSet<>(); Set clusters = new HashSet<>(); @@ -399,7 +396,7 @@ public static String getDirectEdgeId(long competencyId) { return "edge-" + competencyId + "-direct"; } - public LearningObject getRecommendation(LearningPath learningPath) { + public LearningObject getRecommendation(@NotNull LearningPath learningPath) { return learningPath.getCompetencies().stream() .flatMap(competency -> Stream.concat( competency.getLectureUnits().stream() diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java index 3c0968ee3519..5c69f778ca2f 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java @@ -62,15 +62,13 @@ public LearningPathResource(CourseRepository courseRepository, AuthorizationChec public ResponseEntity enableLearningPathsForCourse(@PathVariable Long courseId) { log.debug("REST request to enable learning paths for course with id: {}", courseId); Course course = courseRepository.findWithEagerCompetenciesByIdElseThrow(courseId); - User user = userRepository.getUserWithGroupsAndAuthorities(); - authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, user); + authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null); if (course.getLearningPathsEnabled()) { throw new BadRequestException("Learning paths are already enabled for this course."); } + course.setLearningPathsEnabled(true); learningPathService.generateLearningPaths(course); - - course.setLeanringPathsEnabled(true); course = courseRepository.save(course); return ResponseEntity.ok(course); @@ -88,8 +86,7 @@ public ResponseEntity enableLearningPathsForCourse(@PathVariable Long co public ResponseEntity> getLearningPathsOnPage(@PathVariable Long courseId, PageableSearchDTO search) { log.debug("REST request to get learning paths for course with id: {}", courseId); Course course = courseRepository.findByIdElseThrow(courseId); - User user = userRepository.getUserWithGroupsAndAuthorities(); - authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, user); + authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null); if (!course.getLearningPathsEnabled()) { throw new BadRequestException("Learning paths are not enabled for this course."); } @@ -108,11 +105,11 @@ public ResponseEntity> getLearningPathsOnPage( public ResponseEntity getLearningPathId(@PathVariable Long courseId) { log.debug("REST request to get learning path id for course with id: {}", courseId); Course course = courseRepository.findByIdElseThrow(courseId); - User user = userRepository.getUserWithGroupsAndAuthorities(); - authorizationCheckService.isStudentInCourse(course, user); + authorizationCheckService.isStudentInCourse(course, null); if (!course.getLearningPathsEnabled()) { throw new BadRequestException("Learning paths are not enabled for this course."); } + User user = userRepository.getUser(); LearningPath learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), user.getId()); return ResponseEntity.ok(learningPath.getId()); } @@ -132,13 +129,13 @@ public ResponseEntity getNgxLearningPath(@PathVariable Long if (!course.getLearningPathsEnabled()) { throw new BadRequestException("Learning paths are not enabled for this course."); } - User user = userRepository.getUserWithGroupsAndAuthorities(); - if (authorizationCheckService.isStudentInCourse(course, user)) { + if (authorizationCheckService.isStudentInCourse(course, null)) { + final var user = userRepository.getUser(); if (!user.getId().equals(learningPath.getUser().getId())) { throw new AccessForbiddenException("You are not allowed to access another users learning path."); } } - else if (!authorizationCheckService.isAtLeastInstructorInCourse(course, user) && !authorizationCheckService.isAdmin()) { + else if (!authorizationCheckService.isAtLeastInstructorInCourse(course, null) && !authorizationCheckService.isAdmin()) { throw new AccessForbiddenException("You are not allowed to access another users learning path."); } NgxLearningPathDTO graph = learningPathService.generateNgxRepresentation(learningPath); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/NgxLearningPathDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/NgxLearningPathDTO.java index 9767380451be..5fec75ccb92c 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/NgxLearningPathDTO.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/NgxLearningPathDTO.java @@ -1,6 +1,5 @@ package de.tum.in.www1.artemis.web.rest.dto.learningpath; -import java.util.Objects; import java.util.Set; import com.fasterxml.jackson.annotation.JsonInclude; @@ -11,24 +10,6 @@ @JsonInclude(JsonInclude.Include.NON_EMPTY) public record NgxLearningPathDTO(Set nodes, Set edges, Set clusters) { - @Override - public int hashCode() { - return Objects.hash(nodes, edges, clusters); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof NgxLearningPathDTO other)) { - return false; - } - return nodes.equals(other.nodes) && edges.equals(other.edges) && clusters.equals(other.clusters); - } - - @Override - public String toString() { - return "NgxLearningPathDTO{nodes=" + nodes + ", edges=" + edges + ", clusters=" + clusters + "}"; - } - public record Node(String id, NodeType type, long linkedResource, boolean completed, String label) { public Node(String id, NodeType type, long linkedResource, String label) { @@ -46,66 +27,12 @@ public Node(String id, NodeType type, long linkedResource) { public Node(String id, NodeType type) { this(id, type, -1); } - - @Override - public int hashCode() { - return Objects.hashCode(id); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof Node other)) { - return false; - } - return id.equals(other.id) && type.equals(other.type) && linkedResource == other.linkedResource && label.equals(other.label); - } - - @Override - public String toString() { - return "Node{id=" + id + ", type=" + type.name() + ", linkedResource=" + linkedResource + ", completed=" + completed + ", label=" + label + "}"; - } } public record Edge(String id, String source, String target) { - - @Override - public int hashCode() { - return Objects.hashCode(id); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof Edge other)) { - return false; - } - return id.equals(other.id) && source.equals(other.source) && target.equals(other.target); - } - - @Override - public String toString() { - return "Edge{id=" + id + ", source=" + source + ", target=" + target + "}"; - } } public record Cluster(String id, String label, Set childNodeIds) { - - @Override - public int hashCode() { - return Objects.hashCode(id); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof Cluster other)) { - return false; - } - return id.equals(other.id) && label.equals(other.label) && childNodeIds.equals(other.childNodeIds); - } - - @Override - public String toString() { - return "Cluster{id=" + id + ", label=" + label + ", childNodeIds=" + childNodeIds + "}"; - } } public enum NodeType { diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index 351c0edf3bc7..056d0bac0f47 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -43,10 +43,10 @@ - + diff --git a/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java b/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java index ed19e2442d81..9c116978e58e 100644 --- a/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java @@ -40,7 +40,7 @@ public class LearningPathUtilService { public Course enableAndGenerateLearningPathsForCourse(Course course) { var eagerlyLoadedCourse = courseRepository.findWithEagerLearningPathsAndCompetenciesByIdElseThrow(course.getId()); learningPathService.generateLearningPaths(eagerlyLoadedCourse); - eagerlyLoadedCourse.setLeanringPathsEnabled(true); + eagerlyLoadedCourse.setLearningPathsEnabled(true); return courseRepository.save(eagerlyLoadedCourse); } diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java index 211ce23947c4..49b39ff537c5 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java @@ -223,7 +223,7 @@ void testEnableLearningPathsWithNoCompetencies() throws Exception { @Test @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") void testEnableLearningPathsAlreadyEnabled() throws Exception { - course.setLeanringPathsEnabled(true); + course.setLearningPathsEnabled(true); courseRepository.save(course); request.putWithResponseBody("/api/courses/" + course.getId() + "/learning-paths/enable", course, Course.class, HttpStatus.BAD_REQUEST); } @@ -358,7 +358,7 @@ void testGetNgxLearningPathForLearningPathsDisabled() throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); final var student = userRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); - course.setLeanringPathsEnabled(false); + course.setLearningPathsEnabled(false); courseRepository.save(course); request.get("/api/learning-path/" + learningPath.getId(), HttpStatus.BAD_REQUEST, NgxLearningPathDTO.class); } From 8c25501d2a2d0102eaf3adf7cb0a46f16becea43 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Mon, 24 Jul 2023 11:42:21 +0200 Subject: [PATCH 067/215] fix load learning paths after enable --- .../learning-path-management.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts index 46526a35925b..d831ae230e4c 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts @@ -154,7 +154,7 @@ export class LearningPathManagementComponent implements OnInit, OnDestroy { .enableLearningPaths(this.courseId) .pipe( finalize(() => { - this.isLoading = false; + this.loadData(); }), ) .subscribe({ From 6d4995630f6f04e4856e79b427473a6c81446add Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Mon, 24 Jul 2023 13:59:24 +0200 Subject: [PATCH 068/215] add navigation to progress view --- .../learning-path-graph.component.ts | 5 +++++ .../learning-path-progress-modal.component.html | 11 ++++++++++- .../learning-path-progress-modal.component.scss | 13 +++++++++++++ .../learning-path-progress-modal.component.ts | 4 +++- .../learning-path-progress-nav.component.html | 11 +++++++++++ .../learning-path-progress-nav.component.ts | 17 +++++++++++++++++ .../learning-paths/learning-paths.module.ts | 4 +++- 7 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-nav.component.html create mode 100644 src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-nav.component.ts diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts index 860236e0481b..b1ed386de835 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts @@ -50,4 +50,9 @@ export class LearningPathGraphComponent implements OnInit { this.center$.next(true); this.zoomToFit$.next(true); } + + onCenterView() { + this.zoomToFit$.next(true); + this.center$.next(true); + } } diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.html b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.html index 5d15f51178bf..4b38a926a616 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.html @@ -1,3 +1,12 @@ diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.scss b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.scss index 813a13004d4a..5db8905472ca 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.scss +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.scss @@ -1,6 +1,19 @@ .modal-container { display: flex; + flex-wrap: wrap; height: 90vh; + width: 100%; + padding-top: 8px; + + .row { + width: 100%; + margin-left: 0; + margin-right: 0; + } + + .modal-nav { + height: max-content; + } .graph { width: 100%; diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.ts index 478979f37922..80fb732c23d3 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.ts @@ -1,5 +1,6 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, OnInit, ViewChild } from '@angular/core'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { LearningPathGraphComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph.component'; @Component({ selector: 'jhi-learning-path-progress-modal', styleUrls: ['./learning-path-progress-modal.component.scss'], @@ -7,6 +8,7 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; }) export class LearningPathProgressModalComponent { @Input() learningPathId: number; + @ViewChild('learningPathGraphComponent') learningPathGraphComponent: LearningPathGraphComponent; constructor(private activeModal: NgbActiveModal) {} close() { diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-nav.component.html b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-nav.component.html new file mode 100644 index 000000000000..c56b9512b4e9 --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-nav.component.html @@ -0,0 +1,11 @@ +
    +
    + +
    +
    + +
    +
    + +
    +
    diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-nav.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-nav.component.ts new file mode 100644 index 000000000000..3d968549da59 --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-nav.component.ts @@ -0,0 +1,17 @@ +import { Component, EventEmitter, Output } from '@angular/core'; +import { faArrowsRotate, faArrowsToEye, faXmark } from '@fortawesome/free-solid-svg-icons'; + +@Component({ + selector: 'jhi-learning-path-progress-nav', + templateUrl: './learning-path-progress-nav.component.html', +}) +export class LearningPathProgressNavComponent { + @Output() onRefresh: EventEmitter = new EventEmitter(); + @Output() onCenterView: EventEmitter = new EventEmitter(); + @Output() onClose: EventEmitter = new EventEmitter(); + + // icons + faXmark = faXmark; + faArrowsToEye = faArrowsToEye; + faArrowsRotate = faArrowsRotate; +} diff --git a/src/main/webapp/app/course/learning-paths/learning-paths.module.ts b/src/main/webapp/app/course/learning-paths/learning-paths.module.ts index 7f03e4c6e2f0..3d635a6ea790 100644 --- a/src/main/webapp/app/course/learning-paths/learning-paths.module.ts +++ b/src/main/webapp/app/course/learning-paths/learning-paths.module.ts @@ -13,6 +13,7 @@ import { LearningPathGraphNodeComponent } from 'app/course/learning-paths/learni import { NgxGraphModule } from '@swimlane/ngx-graph'; import { ArtemisLectureUnitsModule } from 'app/overview/course-lectures/lecture-units.module'; import { LearningPathProgressModalComponent } from 'app/course/learning-paths/learning-path-management/learning-path-progress-modal.component'; +import { LearningPathProgressNavComponent } from 'app/course/learning-paths/learning-path-management/learning-path-progress-nav.component'; const routes: Routes = [ { @@ -58,10 +59,11 @@ const routes: Routes = [ declarations: [ LearningPathContainerComponent, LearningPathManagementComponent, + LearningPathProgressModalComponent, + LearningPathProgressNavComponent, LearningPathGraphSidebarComponent, LearningPathGraphComponent, LearningPathGraphNodeComponent, - LearningPathProgressModalComponent, ], exports: [LearningPathContainerComponent], }) From e829eb2831c87c916994e73c67f314a38ec688f2 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Mon, 24 Jul 2023 15:12:56 +0200 Subject: [PATCH 069/215] fix learning content style --- .../learning-path-container.component.html | 4 ++-- .../learning-path-container.component.scss | 13 +++++++++++++ .../learning-path-container.component.ts | 4 +++- 3 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.scss diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html index 8c24c96375eb..ee54c1faa122 100644 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html @@ -1,9 +1,9 @@ -
    +
    -
    +
    No task selected
    diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.scss b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.scss new file mode 100644 index 000000000000..7b9a664c9b4b --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.scss @@ -0,0 +1,13 @@ +.main-container { + width: 100%; + margin-left: 0; +} + +.course-info-bar { + margin: 0 !important; +} + +.tab-bar-exercise-details { + margin-left: 0 !important; + margin-right: 0 !important; +} diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts index 53793b40c7d5..6077305a828a 100644 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons'; import { Exercise } from 'app/entities/exercise.model'; @@ -16,7 +16,9 @@ import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service' @Component({ selector: 'jhi-learning-path-container', + styleUrls: ['./learning-path-container.component.scss'], templateUrl: './learning-path-container.component.html', + encapsulation: ViewEncapsulation.None, }) export class LearningPathContainerComponent implements OnInit { @Input() courseId: number; From 7e5e47b748dee6e564dbe1ef37469a72e5fc6c5a Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Tue, 25 Jul 2023 13:52:38 +0200 Subject: [PATCH 070/215] add improvements --- .../learning-path-graph-node.component.html | 35 +++++- .../learning-path-graph-node.component.ts | 7 +- .../learning-path-graph.component.html | 2 +- .../learning-path-graph.component.scss | 22 ++++ .../learning-path-graph.component.ts | 6 +- .../competency-node-details.component.html | 18 +++ .../competency-node-details.component.ts | 58 ++++++++++ .../exercise-node-details.component.html | 0 .../exercise-node-details.component.ts | 35 ++++++ .../lecture-unit-node-details.component.html | 14 +++ .../lecture-unit-node-details.component.ts | 42 +++++++ .../learning-path-management.component.ts | 1 + ...earning-path-progress-modal.component.html | 2 +- .../learning-path-progress-modal.component.ts | 1 + .../learning-paths/learning-paths.module.ts | 18 ++- .../lecture-unit/lectureUnit.model.ts | 35 +++++- src/main/webapp/app/shared/shared.module.ts | 3 + .../sticky-popover.directive.ts | 105 ++++++++++++++++++ 18 files changed, 394 insertions(+), 10 deletions(-) create mode 100644 src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/competency-node-details.component.html create mode 100644 src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/competency-node-details.component.ts create mode 100644 src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/exercise-node-details.component.html create mode 100644 src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/exercise-node-details.component.ts create mode 100644 src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/lecture-unit-node-details.component.html create mode 100644 src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/lecture-unit-node-details.component.ts create mode 100644 src/main/webapp/app/shared/sticky-popover/sticky-popover.directive.ts diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.html b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.html index dc85250d2733..ba069046e5d7 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.html @@ -1,7 +1,36 @@ -
    - +
    +
    - +
    + +
    +
    + + +
    + +
    +
    + + + + + + + + diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.ts index 39d2baf34dbe..f18d36471db6 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.ts @@ -1,12 +1,14 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, ViewChild } from '@angular/core'; import { faCheckCircle, faCircle, faPlayCircle, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; import { NgxLearningPathNode, NodeType } from 'app/entities/learning-path.model'; +import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'jhi-learning-path-graph-node', templateUrl: './learning-path-graph-node.component.html', }) export class LearningPathGraphNodeComponent { + @Input() courseId: number; @Input() node: NgxLearningPathNode; //icons @@ -16,4 +18,7 @@ export class LearningPathGraphNodeComponent { faCircle = faCircle; protected readonly NodeType = NodeType; + + @ViewChild('inspectPopover') + private inspectPopover: NgbPopover; } diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.html b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.html index 8da15aa6ade3..c72307fe6a29 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.html @@ -23,7 +23,7 @@ - + diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.scss b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.scss index 0a66e3296ee5..8b931cbdf212 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.scss +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.scss @@ -13,3 +13,25 @@ height: 100%; } } + +jhi-learning-path-graph-node:hover { + cursor: pointer; +} + +.node-icon-container { + width: 100%; + display: flex; + + fa-icon { + width: 100%; + svg { + margin: 10%; + width: 80%; + height: 80%; + } + } +} + +.completed { + color: var(--bs-success); +} diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts index 860236e0481b..dc13c262d8ad 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts @@ -1,20 +1,22 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Layout } from '@swimlane/ngx-graph'; import * as shape from 'd3-shape'; import { Subject } from 'rxjs'; import { LearningPathService } from 'app/course/learning-paths/learning-path.service'; -import { NgxLearningPathDTO } from 'app/entities/learning-path.model'; +import { NgxLearningPathDTO, NgxLearningPathNode } from 'app/entities/learning-path.model'; @Component({ selector: 'jhi-learning-path-graph', styleUrls: ['./learning-path-graph.component.scss'], templateUrl: './learning-path-graph.component.html', + encapsulation: ViewEncapsulation.None, }) export class LearningPathGraphComponent implements OnInit { isLoading = false; @Input() learningPathId: number; @Input() courseId: number; + @Output() nodeClicked: EventEmitter = new EventEmitter(); ngxLearningPath: NgxLearningPathDTO; layout: string | Layout = 'dagreCluster'; diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/competency-node-details.component.html b/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/competency-node-details.component.html new file mode 100644 index 000000000000..b84a4cc2cfcf --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/competency-node-details.component.html @@ -0,0 +1,18 @@ +
    +
    +

    + + {{ competency!.title }} + Mastered + Optional +

    +
    {{ competency.description }}
    +
    +
    +
    diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/competency-node-details.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/competency-node-details.component.ts new file mode 100644 index 000000000000..24cb6455586b --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/competency-node-details.component.ts @@ -0,0 +1,58 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { HttpErrorResponse } from '@angular/common/http'; +import { onError } from 'app/shared/util/global.utils'; +import { CompetencyService } from 'app/course/competencies/competency.service'; +import { Competency, CompetencyProgress, getIcon, getIconTooltip } from 'app/entities/competency.model'; +import { AlertService } from 'app/core/util/alert.service'; + +@Component({ + selector: 'jhi-competency-node-details', + templateUrl: './competency-node-details.component.html', +}) +export class CompetencyNodeDetailsComponent implements OnInit { + @Input() courseId: number; + @Input() competencyId: number; + competency: Competency; + competencyProgress: CompetencyProgress; + + isLoading = false; + + constructor(private competencyService: CompetencyService, private alertService: AlertService) {} + + ngOnInit() { + if (this.competencyId && this.courseId) { + this.loadData(); + } + } + private loadData() { + this.isLoading = true; + this.competencyService.findById(this.competencyId!, this.courseId!).subscribe({ + next: (resp) => { + this.competency = resp.body!; + if (this.competency.userProgress?.length) { + this.competencyProgress = this.competency.userProgress.first()!; + } else { + this.competencyProgress = { progress: 0, confidence: 0 } as CompetencyProgress; + } + this.isLoading = false; + }, + error: (errorResponse: HttpErrorResponse) => onError(this.alertService, errorResponse), + }); + } + + get progress(): number { + return Math.round(this.competencyProgress.progress ?? 0); + } + + get confidence(): number { + return Math.min(Math.round(((this.competencyProgress.confidence ?? 0) / (this.competency.masteryThreshold ?? 100)) * 100), 100); + } + + get mastery(): number { + const weight = 2 / 3; + return Math.round((1 - weight) * this.progress + weight * this.confidence); + } + + protected readonly getIcon = getIcon; + protected readonly getIconTooltip = getIconTooltip; +} diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/exercise-node-details.component.html b/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/exercise-node-details.component.html new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/exercise-node-details.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/exercise-node-details.component.ts new file mode 100644 index 000000000000..5f081d966294 --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/exercise-node-details.component.ts @@ -0,0 +1,35 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { HttpErrorResponse } from '@angular/common/http'; +import { onError } from 'app/shared/util/global.utils'; +import { AlertService } from 'app/core/util/alert.service'; +import { Exercise } from 'app/entities/exercise.model'; +import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; + +@Component({ + selector: 'jhi-exercise-node-details', + templateUrl: './exercise-node-details.component.html', +}) +export class ExerciseNodeDetailsComponent implements OnInit { + @Input() exerciseId: number; + + exercise: Exercise; + + isLoading = false; + + constructor(private exerciseService: ExerciseService, private alertService: AlertService) {} + + ngOnInit() { + if (this.exerciseId) { + this.loadData(); + } + } + private loadData() { + this.isLoading = true; + this.exerciseService.find(this.exerciseId).subscribe({ + next: (exerciseResponse) => { + this.exercise = exerciseResponse.body!; + }, + error: (errorResponse: HttpErrorResponse) => onError(this.alertService, errorResponse), + }); + } +} diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/lecture-unit-node-details.component.html b/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/lecture-unit-node-details.component.html new file mode 100644 index 000000000000..fe46e533a422 --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/lecture-unit-node-details.component.html @@ -0,0 +1,14 @@ +
    +
    +

    + + {{ lectureUnit.name }} +

    +
    +
    diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/lecture-unit-node-details.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/lecture-unit-node-details.component.ts new file mode 100644 index 000000000000..7c8a5245b9d2 --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/lecture-unit-node-details.component.ts @@ -0,0 +1,42 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { HttpErrorResponse } from '@angular/common/http'; +import { onError } from 'app/shared/util/global.utils'; +import { AlertService } from 'app/core/util/alert.service'; +import { LectureService } from 'app/lecture/lecture.service'; +import { Lecture } from 'app/entities/lecture.model'; +import { LectureUnit } from 'app/entities/lecture-unit/lectureUnit.model'; + +@Component({ + selector: 'jhi-lecture-unit-node-details', + templateUrl: './lecture-unit-node-details.component.html', +}) +export class LectureUnitNodeDetailsComponent implements OnInit { + @Input() lectureId: number; + @Input() lectureUnitId: number; + + lecture: Lecture; + lectureUnit: LectureUnit; + + isLoading = false; + + constructor(private lectureService: LectureService, private alertService: AlertService) {} + + ngOnInit() { + if (this.lectureId && this.lectureUnitId) { + this.loadData(); + } + } + private loadData() { + this.isLoading = true; + this.lectureService.findWithDetails(this.lectureId!).subscribe({ + next: (findLectureResult) => { + this.lecture = findLectureResult.body!; + if (this.lecture?.lectureUnits) { + this.lectureUnit = this.lecture.lectureUnits.find((lectureUnit) => lectureUnit.id === this.lectureUnitId)!; + } + this.isLoading = false; + }, + error: (errorResponse: HttpErrorResponse) => onError(this.alertService, errorResponse), + }); + } +} diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts index 46526a35925b..4753bb24d43b 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts @@ -210,6 +210,7 @@ export class LearningPathManagementComponent implements OnInit, OnDestroy { backdrop: 'static', windowClass: 'learning-path-modal', }); + modalRef.componentInstance.courseId = this.courseId; modalRef.componentInstance.learningPathId = learningPath.id; } } diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.html b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.html index 5d15f51178bf..fb22130021fe 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.html @@ -1,3 +1,3 @@ diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.ts index 478979f37922..5a7e48201527 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.ts @@ -6,6 +6,7 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; templateUrl: './learning-path-progress-modal.component.html', }) export class LearningPathProgressModalComponent { + @Input() courseId: number; @Input() learningPathId: number; constructor(private activeModal: NgbActiveModal) {} diff --git a/src/main/webapp/app/course/learning-paths/learning-paths.module.ts b/src/main/webapp/app/course/learning-paths/learning-paths.module.ts index 7f03e4c6e2f0..dd2ebd8beba4 100644 --- a/src/main/webapp/app/course/learning-paths/learning-paths.module.ts +++ b/src/main/webapp/app/course/learning-paths/learning-paths.module.ts @@ -13,6 +13,10 @@ import { LearningPathGraphNodeComponent } from 'app/course/learning-paths/learni import { NgxGraphModule } from '@swimlane/ngx-graph'; import { ArtemisLectureUnitsModule } from 'app/overview/course-lectures/lecture-units.module'; import { LearningPathProgressModalComponent } from 'app/course/learning-paths/learning-path-management/learning-path-progress-modal.component'; +import { CompetencyNodeDetailsComponent } from 'app/course/learning-paths/learning-path-graph/node-details/competency-node-details.component'; +import { ArtemisCompetenciesModule } from 'app/course/competencies/competency.module'; +import { LectureUnitNodeDetailsComponent } from 'app/course/learning-paths/learning-path-graph/node-details/lecture-unit-node-details.component'; +import { ExerciseNodeDetailsComponent } from 'app/course/learning-paths/learning-path-graph/node-details/exercise-node-details.component'; const routes: Routes = [ { @@ -54,7 +58,16 @@ const routes: Routes = [ ]; @NgModule({ - imports: [ArtemisSharedModule, FormsModule, ReactiveFormsModule, ArtemisSharedComponentModule, NgxGraphModule, RouterModule.forChild(routes), ArtemisLectureUnitsModule], + imports: [ + ArtemisSharedModule, + FormsModule, + ReactiveFormsModule, + ArtemisSharedComponentModule, + NgxGraphModule, + RouterModule.forChild(routes), + ArtemisLectureUnitsModule, + ArtemisCompetenciesModule, + ], declarations: [ LearningPathContainerComponent, LearningPathManagementComponent, @@ -62,6 +75,9 @@ const routes: Routes = [ LearningPathGraphComponent, LearningPathGraphNodeComponent, LearningPathProgressModalComponent, + CompetencyNodeDetailsComponent, + LectureUnitNodeDetailsComponent, + ExerciseNodeDetailsComponent, ], exports: [LearningPathContainerComponent], }) diff --git a/src/main/webapp/app/entities/lecture-unit/lectureUnit.model.ts b/src/main/webapp/app/entities/lecture-unit/lectureUnit.model.ts index 647d462e0cb7..e47782e08fee 100644 --- a/src/main/webapp/app/entities/lecture-unit/lectureUnit.model.ts +++ b/src/main/webapp/app/entities/lecture-unit/lectureUnit.model.ts @@ -1,7 +1,9 @@ import { BaseEntity } from 'app/shared/model/base-entity'; import dayjs from 'dayjs/esm'; import { Lecture } from 'app/entities/lecture.model'; -import { Competency } from 'app/entities/competency.model'; +import { Competency, CompetencyTaxonomy } from 'app/entities/competency.model'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { faBrain, faComments, faDownload, faLink, faQuestion, faScroll, faVideo } from '@fortawesome/free-solid-svg-icons'; // IMPORTANT NOTICE: The following strings have to be consistent with // the ones defined in LectureUnit.java @@ -13,6 +15,22 @@ export enum LectureUnitType { ONLINE = 'online', } +export const lectureUnitIcons = { + [LectureUnitType.ATTACHMENT]: faDownload, + [LectureUnitType.EXERCISE]: faQuestion, + [LectureUnitType.TEXT]: faScroll, + [LectureUnitType.VIDEO]: faVideo, + [LectureUnitType.ONLINE]: faLink, +}; + +export const lectureUnitTooltips = { + [LectureUnitType.ATTACHMENT]: 'artemisApp.attachmentUnit.tooltip', + [LectureUnitType.EXERCISE]: '', + [LectureUnitType.TEXT]: 'artemisApp.textUnit.tooltip', + [LectureUnitType.VIDEO]: 'artemisApp.videoUnit.tooltip', + [LectureUnitType.ONLINE]: 'artemisApp.onlineUnit.tooltip', +}; + export abstract class LectureUnit implements BaseEntity { public id?: number; public name?: string; @@ -27,4 +45,19 @@ export abstract class LectureUnit implements BaseEntity { protected constructor(type: LectureUnitType) { this.type = type; } + + get getIcon(): IconProp { + if (!this.type) { + return faQuestion as IconProp; + } + return lectureUnitIcons[this.type] as IconProp; + } + + getIconTooltip(): string { + if (!this.type) { + return ''; + } + + return lectureUnitTooltips[this.type]; + } } diff --git a/src/main/webapp/app/shared/shared.module.ts b/src/main/webapp/app/shared/shared.module.ts index 108a76462502..774efdf960a3 100644 --- a/src/main/webapp/app/shared/shared.module.ts +++ b/src/main/webapp/app/shared/shared.module.ts @@ -23,6 +23,7 @@ import { AssessmentWarningComponent } from 'app/assessment/assessment-warning/as import { JhiConnectionWarningComponent } from 'app/shared/connection-warning/connection-warning.component'; import { LoadingIndicatorContainerComponent } from 'app/shared/loading-indicator-container/loading-indicator-container.component'; import { CompetencySelectionComponent } from 'app/shared/competency-selection/competency-selection.component'; +import { StickyPopoverDirective } from 'app/shared/sticky-popover/sticky-popover.directive'; @NgModule({ imports: [ArtemisSharedLibsModule, ArtemisSharedCommonModule, ArtemisSharedPipesModule, RouterModule], @@ -47,6 +48,7 @@ import { CompetencySelectionComponent } from 'app/shared/competency-selection/co ItemCountComponent, ConsistencyCheckComponent, AssessmentWarningComponent, + StickyPopoverDirective, ], exports: [ ArtemisSharedLibsModule, @@ -72,6 +74,7 @@ import { CompetencySelectionComponent } from 'app/shared/competency-selection/co ConsistencyCheckComponent, AssessmentWarningComponent, CompetencySelectionComponent, + StickyPopoverDirective, ], }) export class ArtemisSharedModule {} diff --git a/src/main/webapp/app/shared/sticky-popover/sticky-popover.directive.ts b/src/main/webapp/app/shared/sticky-popover/sticky-popover.directive.ts new file mode 100644 index 000000000000..4950c06f7937 --- /dev/null +++ b/src/main/webapp/app/shared/sticky-popover/sticky-popover.directive.ts @@ -0,0 +1,105 @@ +import { + ApplicationRef, + ChangeDetectorRef, + Directive, + ElementRef, + Inject, + Injector, + Input, + NgZone, + OnDestroy, + OnInit, + Renderer2, + TemplateRef, + ViewContainerRef, +} from '@angular/core'; + +import { DOCUMENT } from '@angular/common'; +import { NgbPopover, NgbPopoverConfig } from '@ng-bootstrap/ng-bootstrap'; +@Directive({ + selector: '[jhiStickyPopover]', +}) +export class StickyPopoverDirective extends NgbPopover implements OnInit, OnDestroy { + @Input() jhiStickyPopover: TemplateRef; + + popoverTitle: string; + + triggers: string; + container: string; + ngpPopover: TemplateRef; + canClosePopover: boolean; + + toggle(): void { + super.toggle(); + } + + isOpen(): boolean { + return super.isOpen(); + } + + constructor( + private _elRef: ElementRef, + private _render: Renderer2, + injector: Injector, + private viewContainerRef: ViewContainerRef, + config: NgbPopoverConfig, + ngZone: NgZone, + private changeRef: ChangeDetectorRef, + private applicationRef: ApplicationRef, + @Inject(DOCUMENT) _document: any, + ) { + super(_elRef, _render, injector, viewContainerRef, config, ngZone, _document, changeRef, applicationRef); + this.triggers = 'manual'; + this.popoverTitle = ''; + this.container = 'body'; + } + + ngOnInit(): void { + super.ngOnInit(); + this.ngbPopover = this.jhiStickyPopover; + + this._render.listen(this._elRef.nativeElement, 'mouseenter', () => { + this.canClosePopover = true; + this.open(); + }); + + this._render.listen(this._elRef.nativeElement, 'mouseleave', () => { + setTimeout(() => { + if (this.canClosePopover) { + this.close(); + } + }, 100); + }); + + this._render.listen(this._elRef.nativeElement, 'click', () => { + this.close(); + }); + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + } + + open() { + super.open(); + setTimeout(() => { + const popover = window.document.querySelector('.popover'); + this._render.listen(popover, 'mouseover', () => { + this.canClosePopover = false; + }); + + this._render.listen(popover, 'mouseout', () => { + this.canClosePopover = true; + setTimeout(() => { + if (this.canClosePopover) { + this.close(); + } + }, 0); + }); + }, 0); + } + + close() { + super.close(); + } +} From ab3fc1c576eb19a85c1581a11d0fdadfdbd3ca6a Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Tue, 25 Jul 2023 14:09:56 +0200 Subject: [PATCH 071/215] add missing JavaDoc --- .../de/tum/in/www1/artemis/service/LearningPathService.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index 3fd0ec8dc887..e62dab7dc611 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -396,6 +396,12 @@ public static String getDirectEdgeId(long competencyId) { return "edge-" + competencyId + "-direct"; } + /** + * Gets a recommended learning object based on the current state of the given learning path. + * + * @param learningPath the learning path for which the recommendation should be computed + * @return recommended learning object + */ public LearningObject getRecommendation(@NotNull LearningPath learningPath) { return learningPath.getCompetencies().stream() .flatMap(competency -> Stream.concat( From 4968662211738030ce36342666f991ec6c21425e Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Wed, 26 Jul 2023 16:45:39 +0200 Subject: [PATCH 072/215] Adapt new DTO restriction --- .../repository/LearningPathRepository.java | 2 +- .../artemis/service/LearningPathService.java | 13 ++++++------- .../web/rest/LearningPathResource.java | 19 ++++++++++--------- .../LearningPathPageableSearchDTO.java | 11 +++++++++++ .../LearningPathRecommendationDTO.java | 8 ++++++++ .../NgxLearningPathDTO.java | 2 +- .../LearningPathRecommendation.java | 8 -------- .../rest/dto/user/UserNameAndLoginDTO.java | 10 ++++++++++ src/main/webapp/app/core/user/user.model.ts | 8 ++++++++ .../learning-path-graph-node.component.ts | 2 +- .../learning-path-graph.component.ts | 2 +- .../learning-path-management.component.html | 2 +- .../learning-path-management.component.ts | 12 ++++++------ .../learning-path-paging.service.ts | 4 ++-- .../learning-paths/learning-path.service.ts | 8 ++++---- .../learning-path-container.component.ts | 2 +- .../{ => competency}/learning-path.model.ts | 10 ++++++++-- src/main/webapp/app/entities/course.model.ts | 2 +- .../lecture/LearningPathIntegrationTest.java | 12 +++++++----- .../service/LearningPathServiceTest.java | 2 +- .../course/course-management.service.spec.ts | 2 +- ...learning-path-graph-node.component.spec.ts | 2 +- ...learning-path-management.component.spec.ts | 2 +- .../learning-path-container.component.spec.ts | 6 +++--- 24 files changed, 94 insertions(+), 57 deletions(-) create mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/LearningPathPageableSearchDTO.java create mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/LearningPathRecommendationDTO.java rename src/main/java/de/tum/in/www1/artemis/web/rest/dto/{learningpath => competency}/NgxLearningPathDTO.java (95%) delete mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/LearningPathRecommendation.java create mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/dto/user/UserNameAndLoginDTO.java rename src/main/webapp/app/entities/{ => competency}/learning-path.model.ts (85%) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java index 4a08dfcc341a..81acb13fbff2 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java @@ -44,7 +44,7 @@ default LearningPath findWithEagerCompetenciesByCourseIdAndUserIdElseThrow(long @Query(""" SELECT lp FROM LearningPath lp - WHERE (lp.course.id = :courseId) AND (lp.user.login LIKE %:searchTerm% OR lp.user.firstName LIKE %:searchTerm% OR lp.user.lastName LIKE %:searchTerm%) + WHERE (lp.course.id = :courseId) AND (lp.user.login LIKE %:searchTerm% OR CONCAT(lp.user.firstName, ' ', lp.user.lastName) LIKE %:searchTerm%) """) Page findByLoginOrNameInCourse(@Param("searchTerm") String searchTerm, @Param("courseId") long courseId, Pageable pageable); diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index e62dab7dc611..9a2fc5ca4922 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -1,9 +1,6 @@ package de.tum.in.www1.artemis.service; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import java.util.stream.LongStream; @@ -26,7 +23,8 @@ import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.web.rest.dto.PageableSearchDTO; import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; -import de.tum.in.www1.artemis.web.rest.dto.learningpath.NgxLearningPathDTO; +import de.tum.in.www1.artemis.web.rest.dto.competency.LearningPathPageableSearchDTO; +import de.tum.in.www1.artemis.web.rest.dto.competency.NgxLearningPathDTO; import de.tum.in.www1.artemis.web.rest.util.PageUtil; @Service @@ -99,11 +97,12 @@ public void generateLearningPathForUser(@NotNull Course course, @NotNull User us * @param course the course the learning paths are linked to * @return A wrapper object containing a list of all found learning paths and the total number of pages */ - public SearchResultPageDTO getAllOfCourseOnPageWithSize(@NotNull PageableSearchDTO search, @NotNull Course course) { + public SearchResultPageDTO getAllOfCourseOnPageWithSize(@NotNull PageableSearchDTO search, @NotNull Course course) { final var pageable = PageUtil.createLearningPathPageRequest(search); final var searchTerm = search.getSearchTerm(); final Page learningPathPage = learningPathRepository.findByLoginOrNameInCourse(searchTerm, course.getId(), pageable); - return new SearchResultPageDTO<>(learningPathPage.getContent(), learningPathPage.getTotalPages()); + final List contentDTOs = learningPathPage.getContent().stream().map(LearningPathPageableSearchDTO::new).toList(); + return new SearchResultPageDTO<>(contentDTOs, learningPathPage.getTotalPages()); } /** diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java index 5c69f778ca2f..8f07ba2d9332 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java @@ -22,8 +22,9 @@ import de.tum.in.www1.artemis.service.LearningPathService; import de.tum.in.www1.artemis.web.rest.dto.PageableSearchDTO; import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; -import de.tum.in.www1.artemis.web.rest.dto.learningpath.LearningPathRecommendation; -import de.tum.in.www1.artemis.web.rest.dto.learningpath.NgxLearningPathDTO; +import de.tum.in.www1.artemis.web.rest.dto.competency.LearningPathPageableSearchDTO; +import de.tum.in.www1.artemis.web.rest.dto.competency.LearningPathRecommendationDTO; +import de.tum.in.www1.artemis.web.rest.dto.competency.NgxLearningPathDTO; import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException; @RestController @@ -59,7 +60,7 @@ public LearningPathResource(CourseRepository courseRepository, AuthorizationChec */ @PutMapping("/courses/{courseId}/learning-paths/enable") @EnforceAtLeastInstructor - public ResponseEntity enableLearningPathsForCourse(@PathVariable Long courseId) { + public ResponseEntity enableLearningPathsForCourse(@PathVariable Long courseId) { log.debug("REST request to enable learning paths for course with id: {}", courseId); Course course = courseRepository.findWithEagerCompetenciesByIdElseThrow(courseId); authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null); @@ -71,7 +72,7 @@ public ResponseEntity enableLearningPathsForCourse(@PathVariable Long co learningPathService.generateLearningPaths(course); course = courseRepository.save(course); - return ResponseEntity.ok(course); + return ResponseEntity.ok().build(); } /** @@ -83,7 +84,7 @@ public ResponseEntity enableLearningPathsForCourse(@PathVariable Long co */ @GetMapping("/courses/{courseId}/learning-paths") @EnforceAtLeastInstructor - public ResponseEntity> getLearningPathsOnPage(@PathVariable Long courseId, PageableSearchDTO search) { + public ResponseEntity> getLearningPathsOnPage(@PathVariable Long courseId, PageableSearchDTO search) { log.debug("REST request to get learning paths for course with id: {}", courseId); Course course = courseRepository.findByIdElseThrow(courseId); authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null); @@ -150,19 +151,19 @@ else if (!authorizationCheckService.isAtLeastInstructorInCourse(course, null) && */ @GetMapping("/learning-path/{learningPathId}/recommendation") @EnforceAtLeastStudent - public ResponseEntity getRecommendation(@PathVariable Long learningPathId) { + public ResponseEntity getRecommendation(@PathVariable Long learningPathId) { log.debug("REST request to get recommendation for learning path with id: {}", learningPathId); LearningPath learningPath = learningPathRepository.findWithEagerCompetenciesAndLearningObjectsByIdElseThrow(learningPathId); LearningObject recommendation = learningPathService.getRecommendation(learningPath); if (recommendation == null) { - return ResponseEntity.ok(new LearningPathRecommendation(-1, -1, LearningPathRecommendation.RecommendationType.EMPTY)); + return ResponseEntity.ok(new LearningPathRecommendationDTO(-1, -1, LearningPathRecommendationDTO.RecommendationType.EMPTY)); } else if (recommendation instanceof LectureUnit lectureUnit) { return ResponseEntity - .ok(new LearningPathRecommendation(recommendation.getId(), lectureUnit.getLecture().getId(), LearningPathRecommendation.RecommendationType.LECTURE_UNIT)); + .ok(new LearningPathRecommendationDTO(recommendation.getId(), lectureUnit.getLecture().getId(), LearningPathRecommendationDTO.RecommendationType.LECTURE_UNIT)); } else { - return ResponseEntity.ok(new LearningPathRecommendation(recommendation.getId(), -1, LearningPathRecommendation.RecommendationType.EXERCISE)); + return ResponseEntity.ok(new LearningPathRecommendationDTO(recommendation.getId(), -1, LearningPathRecommendationDTO.RecommendationType.EXERCISE)); } } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/LearningPathPageableSearchDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/LearningPathPageableSearchDTO.java new file mode 100644 index 000000000000..9f49626f34af --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/LearningPathPageableSearchDTO.java @@ -0,0 +1,11 @@ +package de.tum.in.www1.artemis.web.rest.dto.competency; + +import de.tum.in.www1.artemis.domain.competency.LearningPath; +import de.tum.in.www1.artemis.web.rest.dto.user.UserNameAndLoginDTO; + +public record LearningPathPageableSearchDTO(long id, UserNameAndLoginDTO user, int progress) { + + public LearningPathPageableSearchDTO(LearningPath learningPath) { + this(learningPath.getId(), new UserNameAndLoginDTO(learningPath.getUser()), learningPath.getProgress()); + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/LearningPathRecommendationDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/LearningPathRecommendationDTO.java new file mode 100644 index 000000000000..e86c41615620 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/LearningPathRecommendationDTO.java @@ -0,0 +1,8 @@ +package de.tum.in.www1.artemis.web.rest.dto.competency; + +public record LearningPathRecommendationDTO(long learningObjectId, long lectureId, RecommendationType type) { + + public enum RecommendationType { + EMPTY, LECTURE_UNIT, EXERCISE + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/NgxLearningPathDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/NgxLearningPathDTO.java similarity index 95% rename from src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/NgxLearningPathDTO.java rename to src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/NgxLearningPathDTO.java index 5fec75ccb92c..46c16afc2559 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/NgxLearningPathDTO.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/NgxLearningPathDTO.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.web.rest.dto.learningpath; +package de.tum.in.www1.artemis.web.rest.dto.competency; import java.util.Set; diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/LearningPathRecommendation.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/LearningPathRecommendation.java deleted file mode 100644 index a43920d0262c..000000000000 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/learningpath/LearningPathRecommendation.java +++ /dev/null @@ -1,8 +0,0 @@ -package de.tum.in.www1.artemis.web.rest.dto.learningpath; - -public record LearningPathRecommendation(long learningObjectId, long lectureId, RecommendationType type) { - - public enum RecommendationType { - EMPTY, LECTURE_UNIT, EXERCISE - } -} diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/user/UserNameAndLoginDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/user/UserNameAndLoginDTO.java new file mode 100644 index 000000000000..615f36683c81 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/user/UserNameAndLoginDTO.java @@ -0,0 +1,10 @@ +package de.tum.in.www1.artemis.web.rest.dto.user; + +import de.tum.in.www1.artemis.domain.User; + +public record UserNameAndLoginDTO(String name, String login) { + + public UserNameAndLoginDTO(User user) { + this(user.getName(), user.getLogin()); + } +} diff --git a/src/main/webapp/app/core/user/user.model.ts b/src/main/webapp/app/core/user/user.model.ts index 12af0f1ecd85..840aecf34a82 100644 --- a/src/main/webapp/app/core/user/user.model.ts +++ b/src/main/webapp/app/core/user/user.model.ts @@ -64,3 +64,11 @@ export class UserPublicInfoDTO { public isTeachingAssistant?: boolean; public isStudent?: boolean; } + +/** + * A DTO representing a user which contains only the name and login + */ +export class UserNameAndLoginDTO { + public name?: string; + public login?: string; +} diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.ts index 39d2baf34dbe..74412fda79df 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.ts @@ -1,6 +1,6 @@ import { Component, Input } from '@angular/core'; import { faCheckCircle, faCircle, faPlayCircle, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; -import { NgxLearningPathNode, NodeType } from 'app/entities/learning-path.model'; +import { NgxLearningPathNode, NodeType } from 'app/entities/competency/learning-path.model'; @Component({ selector: 'jhi-learning-path-graph-node', diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts index 860236e0481b..fe1bd75e673a 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts @@ -4,7 +4,7 @@ import { Layout } from '@swimlane/ngx-graph'; import * as shape from 'd3-shape'; import { Subject } from 'rxjs'; import { LearningPathService } from 'app/course/learning-paths/learning-path.service'; -import { NgxLearningPathDTO } from 'app/entities/learning-path.model'; +import { NgxLearningPathDTO } from 'app/entities/competency/learning-path.model'; @Component({ selector: 'jhi-learning-path-graph', diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html index f4e59a47562c..0bb39b84f150 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html @@ -54,7 +54,7 @@

    Learning Pa {{ learningPath.id }} - + diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts index 46526a35925b..8b838e2f5a0c 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts @@ -11,7 +11,7 @@ import { AlertService } from 'app/core/util/alert.service'; import { PageableSearch, SearchResult, SortingOrder } from 'app/shared/table/pageable-table'; import { LearningPathPagingService } from 'app/course/learning-paths/learning-path-paging.service'; import { SortService } from 'app/shared/service/sort.service'; -import { LearningPath } from 'app/entities/learning-path.model'; +import { LearningPathPageableSearchDTO } from 'app/entities/competency/learning-path.model'; import { faSort } from '@fortawesome/free-solid-svg-icons'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { LearningPathProgressModalComponent } from 'app/course/learning-paths/learning-path-management/learning-path-progress-modal.component'; @@ -44,7 +44,7 @@ export class LearningPathManagementComponent implements OnInit, OnDestroy { sortingOrder: SortingOrder.ASCENDING, sortedColumn: TableColumn.ID, }; - content: SearchResult; + content: SearchResult; total = 0; private search = new Subject(); @@ -92,7 +92,7 @@ export class LearningPathManagementComponent implements OnInit, OnDestroy { * @param item The item itself * @returns The ID of the item */ - trackId(index: number, item: LearningPath): number { + trackId(index: number, item: LearningPathPageableSearchDTO): number { return item.id!; } @@ -158,8 +158,8 @@ export class LearningPathManagementComponent implements OnInit, OnDestroy { }), ) .subscribe({ - next: (res) => { - this.course = res.body!; + next: () => { + this.course.learningPathsEnabled = true; }, error: (res: HttpErrorResponse) => onError(this.alertService, res), }); @@ -204,7 +204,7 @@ export class LearningPathManagementComponent implements OnInit, OnDestroy { this.page = pageNumber; } } - viewLearningPath(learningPath: LearningPath) { + viewLearningPath(learningPath: LearningPathPageableSearchDTO) { const modalRef = this.modalService.open(LearningPathProgressModalComponent, { size: 'xl', backdrop: 'static', diff --git a/src/main/webapp/app/course/learning-paths/learning-path-paging.service.ts b/src/main/webapp/app/course/learning-paths/learning-path-paging.service.ts index 9b91d9922c1e..8a86f9a56e5a 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-paging.service.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-paging.service.ts @@ -4,9 +4,9 @@ import { PagingService } from 'app/exercises/shared/manage/paging.service'; import { PageableSearch, SearchResult } from 'app/shared/table/pageable-table'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { LearningPath } from 'app/entities/learning-path.model'; +import { LearningPathPageableSearchDTO } from 'app/entities/competency/learning-path.model'; -type EntityResponseType = SearchResult; +type EntityResponseType = SearchResult; @Injectable({ providedIn: 'root' }) export class LearningPathPagingService extends PagingService { public resourceUrl = 'api'; diff --git a/src/main/webapp/app/course/learning-paths/learning-path.service.ts b/src/main/webapp/app/course/learning-paths/learning-path.service.ts index 75f4f7ed6bc4..d20be322b513 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path.service.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { HttpClient, HttpResponse } from '@angular/common/http'; import { Course } from 'app/entities/course.model'; -import { LearningPathRecommendation, NgxLearningPathDTO } from 'app/entities/learning-path.model'; +import { LearningPathRecommendationDTO, NgxLearningPathDTO } from 'app/entities/competency/learning-path.model'; import { map } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) @@ -11,8 +11,8 @@ export class LearningPathService { constructor(private httpClient: HttpClient) {} - enableLearningPaths(courseId: number): Observable> { - return this.httpClient.put(`${this.resourceURL}/courses/${courseId}/learning-paths/enable`, null, { observe: 'response' }); + enableLearningPaths(courseId: number): Observable> { + return this.httpClient.put(`${this.resourceURL}/courses/${courseId}/learning-paths/enable`, null, { observe: 'response' }); } getLearningPathId(courseId: number) { @@ -37,6 +37,6 @@ export class LearningPathService { } getRecommendation(learningPathId: number) { - return this.httpClient.get(`${this.resourceURL}/learning-path/${learningPathId}/recommendation`, { observe: 'response' }); + return this.httpClient.get(`${this.resourceURL}/learning-path/${learningPathId}/recommendation`, { observe: 'response' }); } } diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts index 53793b40c7d5..ea0756b434d1 100644 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts @@ -5,7 +5,7 @@ import { Exercise } from 'app/entities/exercise.model'; import { LectureUnit } from 'app/entities/lecture-unit/lectureUnit.model'; import { Lecture } from 'app/entities/lecture.model'; import { LearningPathService } from 'app/course/learning-paths/learning-path.service'; -import { RecommendationType } from 'app/entities/learning-path.model'; +import { RecommendationType } from 'app/entities/competency/learning-path.model'; import { LectureService } from 'app/lecture/lecture.service'; import { onError } from 'app/shared/util/global.utils'; import { HttpErrorResponse } from '@angular/common/http'; diff --git a/src/main/webapp/app/entities/learning-path.model.ts b/src/main/webapp/app/entities/competency/learning-path.model.ts similarity index 85% rename from src/main/webapp/app/entities/learning-path.model.ts rename to src/main/webapp/app/entities/competency/learning-path.model.ts index 1208eeca9eba..018af1edf2b7 100644 --- a/src/main/webapp/app/entities/learning-path.model.ts +++ b/src/main/webapp/app/entities/competency/learning-path.model.ts @@ -1,6 +1,6 @@ import { BaseEntity } from 'app/shared/model/base-entity'; import { Course } from 'app/entities/course.model'; -import { User } from 'app/core/user/user.model'; +import { User, UserNameAndLoginDTO } from 'app/core/user/user.model'; import { Competency } from 'app/entities/competency.model'; import { ClusterNode, Edge, Node } from '@swimlane/ngx-graph'; @@ -14,6 +14,12 @@ export class LearningPath implements BaseEntity { constructor() {} } +export class LearningPathPageableSearchDTO { + public id?: number; + public user?: UserNameAndLoginDTO; + public progress?: number; +} + export class NgxLearningPathDTO { public nodes: NgxLearningPathNode[]; public edges: NgxLearningPathEdge[]; @@ -48,7 +54,7 @@ export enum NodeType { LECTURE_UNIT = 'LECTURE_UNIT', } -export class LearningPathRecommendation { +export class LearningPathRecommendationDTO { public learningObjectId: number; public lectureId?: number; public type: RecommendationType; diff --git a/src/main/webapp/app/entities/course.model.ts b/src/main/webapp/app/entities/course.model.ts index 9b7b28729f8a..aae8b0627951 100644 --- a/src/main/webapp/app/entities/course.model.ts +++ b/src/main/webapp/app/entities/course.model.ts @@ -11,7 +11,7 @@ import { ProgrammingLanguage } from 'app/entities/programming-exercise.model'; import { OnlineCourseConfiguration } from 'app/entities/online-course-configuration.model'; import { TutorialGroup } from 'app/entities/tutorial-group/tutorial-group.model'; import { TutorialGroupsConfiguration } from 'app/entities/tutorial-group/tutorial-groups-configuration.model'; -import { LearningPath } from 'app/entities/learning-path.model'; +import { LearningPath } from 'app/entities/competency/learning-path.model'; export enum CourseInformationSharingConfiguration { COMMUNICATION_AND_MESSAGING = 'COMMUNICATION_AND_MESSAGING', diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java index 49b39ff537c5..2b35c4ef27f7 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java @@ -36,7 +36,8 @@ import de.tum.in.www1.artemis.service.LectureUnitService; import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.util.PageableSearchUtilService; -import de.tum.in.www1.artemis.web.rest.dto.learningpath.NgxLearningPathDTO; +import de.tum.in.www1.artemis.web.rest.dto.competency.LearningPathPageableSearchDTO; +import de.tum.in.www1.artemis.web.rest.dto.competency.NgxLearningPathDTO; class LearningPathIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { @@ -157,7 +158,7 @@ private void testAllPreAuthorize() throws Exception { } private void enableLearningPathsRESTCall(Course course) throws Exception { - request.putWithResponseBody("/api/courses/" + course.getId() + "/learning-paths/enable", course, Course.class, HttpStatus.OK); + request.put("/api/courses/" + course.getId() + "/learning-paths/enable", null, HttpStatus.OK); } private Competency createCompetencyRESTCall() throws Exception { @@ -257,7 +258,8 @@ private void setupEnrollmentRequestMocks() throws JsonProcessingException, URISy @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") void testGetLearningPathsOnPageForCourseLearningPathsDisabled() throws Exception { final var search = pageableSearchUtilService.configureSearch(""); - request.getSearchResult("/api/courses/" + course.getId() + "/learning-paths", HttpStatus.BAD_REQUEST, LearningPath.class, pageableSearchUtilService.searchMapping(search)); + request.getSearchResult("/api/courses/" + course.getId() + "/learning-paths", HttpStatus.BAD_REQUEST, LearningPathPageableSearchDTO.class, + pageableSearchUtilService.searchMapping(search)); } @Test @@ -265,7 +267,7 @@ void testGetLearningPathsOnPageForCourseLearningPathsDisabled() throws Exception void testGetLearningPathsOnPageForCourseEmpty() throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); final var search = pageableSearchUtilService.configureSearch(STUDENT_OF_COURSE + "SuffixThatAllowsTheResultToBeEmpty"); - final var result = request.getSearchResult("/api/courses/" + course.getId() + "/learning-paths", HttpStatus.OK, LearningPath.class, + final var result = request.getSearchResult("/api/courses/" + course.getId() + "/learning-paths", HttpStatus.OK, LearningPathPageableSearchDTO.class, pageableSearchUtilService.searchMapping(search)); assertThat(result.getResultsOnPage()).isNullOrEmpty(); } @@ -275,7 +277,7 @@ void testGetLearningPathsOnPageForCourseEmpty() throws Exception { void testGetLearningPathsOnPageForCourseExactlyStudent() throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); final var search = pageableSearchUtilService.configureSearch(STUDENT_OF_COURSE); - final var result = request.getSearchResult("/api/courses/" + course.getId() + "/learning-paths", HttpStatus.OK, LearningPath.class, + final var result = request.getSearchResult("/api/courses/" + course.getId() + "/learning-paths", HttpStatus.OK, LearningPathPageableSearchDTO.class, pageableSearchUtilService.searchMapping(search)); assertThat(result.getResultsOnPage()).hasSize(1); } diff --git a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java index 1ccb42a0df64..0fc4c8e02c88 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java @@ -28,7 +28,7 @@ import de.tum.in.www1.artemis.repository.CompetencyRepository; import de.tum.in.www1.artemis.repository.LearningPathRepository; import de.tum.in.www1.artemis.security.SecurityUtils; -import de.tum.in.www1.artemis.web.rest.dto.learningpath.NgxLearningPathDTO; +import de.tum.in.www1.artemis.web.rest.dto.competency.NgxLearningPathDTO; class LearningPathServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { diff --git a/src/test/javascript/spec/component/course/course-management.service.spec.ts b/src/test/javascript/spec/component/course/course-management.service.spec.ts index 6aaf0fa42ad9..30d2ccb42d81 100644 --- a/src/test/javascript/spec/component/course/course-management.service.spec.ts +++ b/src/test/javascript/spec/component/course/course-management.service.spec.ts @@ -26,7 +26,7 @@ import { CourseScores } from 'app/course/course-scores/course-scores'; import { ScoresStorageService } from 'app/course/course-scores/scores-storage.service'; import { CourseStorageService } from 'app/course/manage/course-storage.service'; import { Result } from 'app/entities/result.model'; -import { LearningPath } from 'app/entities/learning-path.model'; +import { LearningPath } from 'app/entities/competency/learning-path.model'; describe('Course Management Service', () => { let courseManagementService: CourseManagementService; diff --git a/src/test/javascript/spec/component/learning-paths/graph/learning-path-graph-node.component.spec.ts b/src/test/javascript/spec/component/learning-paths/graph/learning-path-graph-node.component.spec.ts index 844a9462e972..38490538c73c 100644 --- a/src/test/javascript/spec/component/learning-paths/graph/learning-path-graph-node.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/graph/learning-path-graph-node.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ArtemisTestModule } from '../../../test.module'; import { By } from '@angular/platform-browser'; import { LearningPathGraphNodeComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph-node.component'; -import { NgxLearningPathNode, NodeType } from 'app/entities/learning-path.model'; +import { NgxLearningPathNode, NodeType } from 'app/entities/competency/learning-path.model'; describe('LearningPathGraphNodeComponent', () => { let fixture: ComponentFixture; diff --git a/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts b/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts index b2784bccf534..ea127624ba5f 100644 --- a/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts @@ -3,7 +3,7 @@ import { LearningPathManagementComponent, TableColumn } from 'app/course/learnin import { LearningPathPagingService } from 'app/course/learning-paths/learning-path-paging.service'; import { SortService } from 'app/shared/service/sort.service'; import { PageableSearch, SearchResult, SortingOrder } from 'app/shared/table/pageable-table'; -import { LearningPath } from 'app/entities/learning-path.model'; +import { LearningPath } from 'app/entities/competency/learning-path.model'; import { ArtemisTestModule } from '../../../test.module'; import { MockComponent, MockDirective, MockProvider } from 'ng-mocks'; import { ButtonComponent } from 'app/shared/components/button.component'; diff --git a/src/test/javascript/spec/component/learning-paths/participate/learning-path-container.component.spec.ts b/src/test/javascript/spec/component/learning-paths/participate/learning-path-container.component.spec.ts index a63cd02a0a3f..bc952252b548 100644 --- a/src/test/javascript/spec/component/learning-paths/participate/learning-path-container.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/participate/learning-path-container.component.spec.ts @@ -7,7 +7,7 @@ import { ActivatedRoute, RouterModule } from '@angular/router'; import { HttpResponse } from '@angular/common/http'; import { LearningPathContainerComponent } from 'app/course/learning-paths/participate/learning-path-container.component'; import { LearningPathService } from 'app/course/learning-paths/learning-path.service'; -import { LearningPathRecommendation, RecommendationType } from 'app/entities/learning-path.model'; +import { LearningPathRecommendationDTO, RecommendationType } from 'app/entities/competency/learning-path.model'; import { LectureService } from 'app/lecture/lecture.service'; import { Lecture } from 'app/entities/lecture.model'; import { LectureUnit } from 'app/entities/lecture-unit/lectureUnit.model'; @@ -95,7 +95,7 @@ describe('LearningPathContainerComponent', () => { }); it('should load lecture unit on recommendation', () => { - const recommendation = new LearningPathRecommendation(); + const recommendation = new LearningPathRecommendationDTO(); recommendation.learningObjectId = lectureUnit.id!; recommendation.lectureId = lecture.id; recommendation.type = RecommendationType.LECTURE_UNIT; @@ -107,7 +107,7 @@ describe('LearningPathContainerComponent', () => { }); it('should load exercise on recommendation', () => { - const recommendation = new LearningPathRecommendation(); + const recommendation = new LearningPathRecommendationDTO(); recommendation.learningObjectId = exercise.id!; recommendation.type = RecommendationType.EXERCISE; getRecommendationStub.mockReturnValue(of(new HttpResponse({ body: recommendation }))); From 6740f276d9c798cb79738c517572371b5176e921 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 27 Jul 2023 01:22:59 +0200 Subject: [PATCH 073/215] Fix failing bamboo test --- .../web/rest/LearningPathResource.java | 4 +++ .../lecture/LearningPathIntegrationTest.java | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java index 8f07ba2d9332..37db0dff1934 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java @@ -154,6 +154,10 @@ else if (!authorizationCheckService.isAtLeastInstructorInCourse(course, null) && public ResponseEntity getRecommendation(@PathVariable Long learningPathId) { log.debug("REST request to get recommendation for learning path with id: {}", learningPathId); LearningPath learningPath = learningPathRepository.findWithEagerCompetenciesAndLearningObjectsByIdElseThrow(learningPathId); + final var user = userRepository.getUser(); + if (!user.getId().equals(learningPath.getUser().getId())) { + throw new AccessForbiddenException("You are not allowed to access another users learning path."); + } LearningObject recommendation = learningPathService.getRecommendation(learningPath); if (recommendation == null) { return ResponseEntity.ok(new LearningPathRecommendationDTO(-1, -1, LearningPathRecommendationDTO.RecommendationType.EMPTY)); diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java index 2b35c4ef27f7..011ce9c9bdc7 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java @@ -37,6 +37,7 @@ import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.util.PageableSearchUtilService; import de.tum.in.www1.artemis.web.rest.dto.competency.LearningPathPageableSearchDTO; +import de.tum.in.www1.artemis.web.rest.dto.competency.LearningPathRecommendationDTO; import de.tum.in.www1.artemis.web.rest.dto.competency.NgxLearningPathDTO; class LearningPathIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { @@ -403,4 +404,28 @@ void testGetNgxLearningPathAsInstructor() throws Exception { final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); request.get("/api/learning-path/" + learningPath.getId(), HttpStatus.OK, NgxLearningPathDTO.class); } + + @Test + @WithMockUser(username = TEST_PREFIX + "student2", roles = "USER") + void testGetRecommendationAsOtherUser() throws Exception { + course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); + final var student = userRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); + request.get("/api/learning-path/" + learningPath.getId() + "/recommendation", HttpStatus.FORBIDDEN, LearningPathRecommendationDTO.class); + } + + /** + * This only tests if the end point successfully retrieves the recommendation. The correctness of the response is tested in LearningPathServiceTest. + * + * @throws Exception the request failed + * @see de.tum.in.www1.artemis.service.LearningPathServiceTest + */ + @Test + @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") + void testGetRecommendationAsOwner() throws Exception { + course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); + final var student = userRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); + request.get("/api/learning-path/" + learningPath.getId() + "/recommendation", HttpStatus.OK, LearningPathRecommendationDTO.class); + } } From eb43150376d292ec8db5a6f71a2bd6ee7566aba2 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 27 Jul 2023 10:31:39 +0200 Subject: [PATCH 074/215] Add client tests --- .../learning-path-management.component.html | 1 + .../learning-path-progress-nav.component.html | 6 +- .../learning-path-graph.component.spec.ts | 11 ++++ ...learning-path-management.component.spec.ts | 24 +++++++- ...ning-path-progress-modal.component.spec.ts | 3 +- ...arning-path-progress-nav.component.spec.ts | 57 +++++++++++++++++++ 6 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 src/test/javascript/spec/component/learning-paths/management/learning-path-progress-nav.component.spec.ts diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html index 0bb39b84f150..8a3efb44a500 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html @@ -10,6 +10,7 @@

    Learning Pa Disabled
    +
    - +
    - +

    diff --git a/src/test/javascript/spec/component/learning-paths/graph/learning-path-graph.component.spec.ts b/src/test/javascript/spec/component/learning-paths/graph/learning-path-graph.component.spec.ts index aac3ebb8ad1d..32dcaacea7b2 100644 --- a/src/test/javascript/spec/component/learning-paths/graph/learning-path-graph.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/graph/learning-path-graph.component.spec.ts @@ -48,4 +48,15 @@ describe('LearningPathGraphComponent', () => { expect(zoomToFitStub).toHaveBeenCalledOnce(); expect(zoomToFitStub).toHaveBeenCalledWith(true); }); + + it('should zoom to fit and center on resize', () => { + const zoomToFitStub = jest.spyOn(comp.zoomToFit$, 'next'); + const centerStub = jest.spyOn(comp.center$, 'next'); + fixture.detectChanges(); + comp.onCenterView(); + expect(zoomToFitStub).toHaveBeenCalledOnce(); + expect(zoomToFitStub).toHaveBeenCalledWith(true); + expect(centerStub).toHaveBeenCalledOnce(); + expect(centerStub).toHaveBeenCalledWith(true); + }); }); diff --git a/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts b/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts index ea127624ba5f..78f4b25e8e6b 100644 --- a/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts @@ -15,6 +15,8 @@ import { CourseManagementService } from 'app/course/manage/course-management.ser import { Course } from 'app/entities/course.model'; import { ActivatedRoute } from '@angular/router'; import { HttpResponse } from '@angular/common/http'; +import { By } from '@angular/platform-browser'; +import { LearningPathService } from 'app/course/learning-paths/learning-path.service'; describe('LearningPathManagementComponent', () => { let fixture: ComponentFixture; @@ -29,6 +31,8 @@ describe('LearningPathManagementComponent', () => { let state: PageableSearch; let learningPath: LearningPath; let course: Course; + let learningPathService: LearningPathService; + let enableLearningPathsStub: jest.SpyInstance; beforeEach(() => { TestBed.configureTestingModule({ imports: [ArtemisTestModule, MockComponent(NgbPagination)], @@ -57,6 +61,8 @@ describe('LearningPathManagementComponent', () => { sortService = TestBed.inject(SortService); searchForLearningPathsStub = jest.spyOn(pagingService, 'searchForLearningPaths'); sortByPropertyStub = jest.spyOn(sortService, 'sortByProperty'); + learningPathService = TestBed.inject(LearningPathService); + enableLearningPathsStub = jest.spyOn(learningPathService, 'enableLearningPaths'); }); }); @@ -71,7 +77,6 @@ describe('LearningPathManagementComponent', () => { course = new Course(); course.id = 1; course.learningPathsEnabled = true; - course.learningPaths = [learningPath]; findCourseStub.mockReturnValue(of(new HttpResponse({ body: course }))); searchResult = { numberOfPages: 3, resultsOnPage: [learningPath] }; state = { @@ -103,6 +108,23 @@ describe('LearningPathManagementComponent', () => { }); })); + it('should allow to enable learning paths if learning paths disabled', () => { + course.learningPathsEnabled = false; + findCourseStub.mockReturnValue(of(new HttpResponse({ body: course }))); + fixture.detectChanges(); + comp.ngOnInit(); + expect(comp.course).toEqual(course); + expect(comp.course.learningPathsEnabled).toBeFalsy(); + expect(comp.isLoading).toBeFalsy(); + const button = fixture.debugElement.query(By.css('div')); + console.log(fixture.debugElement); + expect(button).not.toBeNull(); + + button.nativeElement.click(); + expect(enableLearningPathsStub).toHaveBeenCalledOnce(); + expect(enableLearningPathsStub).toHaveBeenCalledWith(course.id); + }); + it('should set content to paging result on sort', fakeAsync(() => { expect(comp.listSorting).toBeTrue(); setStateAndCallOnInit(() => { diff --git a/src/test/javascript/spec/component/learning-paths/management/learning-path-progress-modal.component.spec.ts b/src/test/javascript/spec/component/learning-paths/management/learning-path-progress-modal.component.spec.ts index 69044986dbba..dbe4e026f787 100644 --- a/src/test/javascript/spec/component/learning-paths/management/learning-path-progress-modal.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/management/learning-path-progress-modal.component.spec.ts @@ -5,6 +5,7 @@ import { MockComponent } from 'ng-mocks'; import { LearningPathProgressModalComponent } from 'app/course/learning-paths/learning-path-management/learning-path-progress-modal.component'; import { LearningPathGraphComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph.component'; import { By } from '@angular/platform-browser'; +import { LearningPathProgressNavComponent } from 'app/course/learning-paths/learning-path-management/learning-path-progress-nav.component'; describe('LearningPathProgressModalComponent', () => { let fixture: ComponentFixture; @@ -14,7 +15,7 @@ describe('LearningPathProgressModalComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ArtemisTestModule, MockComponent(LearningPathGraphComponent)], + imports: [ArtemisTestModule, MockComponent(LearningPathGraphComponent), MockComponent(LearningPathProgressNavComponent)], declarations: [LearningPathProgressModalComponent], providers: [], }) diff --git a/src/test/javascript/spec/component/learning-paths/management/learning-path-progress-nav.component.spec.ts b/src/test/javascript/spec/component/learning-paths/management/learning-path-progress-nav.component.spec.ts new file mode 100644 index 000000000000..84eca7eebcb3 --- /dev/null +++ b/src/test/javascript/spec/component/learning-paths/management/learning-path-progress-nav.component.spec.ts @@ -0,0 +1,57 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ArtemisTestModule } from '../../../test.module'; +import { By } from '@angular/platform-browser'; +import { LearningPathProgressNavComponent } from 'app/course/learning-paths/learning-path-management/learning-path-progress-nav.component'; + +describe('LearningPathProgressNavComponent', () => { + let fixture: ComponentFixture; + let comp: LearningPathProgressNavComponent; + let onRefreshStub: jest.SpyInstance; + let onCenterViewStub: jest.SpyInstance; + let onCloseStub: jest.SpyInstance; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule], + declarations: [LearningPathProgressNavComponent], + providers: [], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(LearningPathProgressNavComponent); + comp = fixture.componentInstance; + onRefreshStub = jest.spyOn(comp.onRefresh, 'emit'); + onCenterViewStub = jest.spyOn(comp.onCenterView, 'emit'); + onCloseStub = jest.spyOn(comp.onClose, 'emit'); + fixture.detectChanges(); + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should emit refresh on click', () => { + const button = fixture.debugElement.query(By.css('#refresh-button')); + expect(button).not.toBeNull(); + + button.nativeElement.click(); + expect(onRefreshStub).toHaveBeenCalledOnce(); + }); + + it('should emit center view on click', () => { + const button = fixture.debugElement.query(By.css('#center-button')); + expect(button).not.toBeNull(); + + button.nativeElement.click(); + expect(onCenterViewStub).toHaveBeenCalledOnce(); + }); + + it('should emit close on click', () => { + const button = fixture.debugElement.query(By.css('#close-button')); + expect(button).not.toBeNull(); + + button.nativeElement.click(); + expect(onCloseStub).toHaveBeenCalledOnce(); + }); +}); From 402bdae46f2ae90c0b3a592af0ab43fb90c40609 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 27 Jul 2023 10:44:03 +0200 Subject: [PATCH 075/215] Remove unused property --- .../learning-path-graph-node.component.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.ts index a8c7d08cedfd..9bd4b5584569 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.ts @@ -1,7 +1,6 @@ -import { Component, Input, ViewChild } from '@angular/core'; +import { Component, Input } from '@angular/core'; import { faCheckCircle, faCircle, faPlayCircle, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; import { NgxLearningPathNode, NodeType } from 'app/entities/competency/learning-path.model'; -import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'jhi-learning-path-graph-node', @@ -18,7 +17,4 @@ export class LearningPathGraphNodeComponent { faCircle = faCircle; protected readonly NodeType = NodeType; - - @ViewChild('inspectPopover') - private inspectPopover: NgbPopover; } From a888497b376962364cf9586c83b30d0562cfb570 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 27 Jul 2023 13:28:05 +0200 Subject: [PATCH 076/215] Add client tests --- ...learning-path-graph-node.component.spec.ts | 4 +- .../competency-node-details.component.spec.ts | 69 +++++++++++++++++++ .../exercise-node-details.component.spec.ts | 52 ++++++++++++++ ...ecture-unit-node-details.component.spec.ts | 57 +++++++++++++++ ...ning-path-progress-modal.component.spec.ts | 1 + .../sticky-popover.directive.spec.ts | 60 ++++++++++++++++ 6 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 src/test/javascript/spec/component/learning-paths/graph/node-details/competency-node-details.component.spec.ts create mode 100644 src/test/javascript/spec/component/learning-paths/graph/node-details/exercise-node-details.component.spec.ts create mode 100644 src/test/javascript/spec/component/learning-paths/graph/node-details/lecture-unit-node-details.component.spec.ts create mode 100644 src/test/javascript/spec/directive/sticky-popover.directive.spec.ts diff --git a/src/test/javascript/spec/component/learning-paths/graph/learning-path-graph-node.component.spec.ts b/src/test/javascript/spec/component/learning-paths/graph/learning-path-graph-node.component.spec.ts index 38490538c73c..8371428d66ed 100644 --- a/src/test/javascript/spec/component/learning-paths/graph/learning-path-graph-node.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/graph/learning-path-graph-node.component.spec.ts @@ -1,8 +1,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ArtemisTestModule } from '../../../test.module'; +import { MockDirective } from 'ng-mocks'; import { By } from '@angular/platform-browser'; import { LearningPathGraphNodeComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph-node.component'; import { NgxLearningPathNode, NodeType } from 'app/entities/competency/learning-path.model'; +import { StickyPopoverDirective } from 'app/shared/sticky-popover/sticky-popover.directive'; describe('LearningPathGraphNodeComponent', () => { let fixture: ComponentFixture; @@ -11,7 +13,7 @@ describe('LearningPathGraphNodeComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [ArtemisTestModule], - declarations: [LearningPathGraphNodeComponent], + declarations: [LearningPathGraphNodeComponent, MockDirective(StickyPopoverDirective)], providers: [], }) .compileComponents() diff --git a/src/test/javascript/spec/component/learning-paths/graph/node-details/competency-node-details.component.spec.ts b/src/test/javascript/spec/component/learning-paths/graph/node-details/competency-node-details.component.spec.ts new file mode 100644 index 000000000000..848c9a2cefe9 --- /dev/null +++ b/src/test/javascript/spec/component/learning-paths/graph/node-details/competency-node-details.component.spec.ts @@ -0,0 +1,69 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ArtemisTestModule } from '../../../../test.module'; +import { MockComponent, MockPipe } from 'ng-mocks'; +import { of } from 'rxjs'; +import { HttpResponse } from '@angular/common/http'; +import { CompetencyNodeDetailsComponent } from 'app/course/learning-paths/learning-path-graph/node-details/competency-node-details.component'; +import { Competency, CompetencyProgress, CompetencyTaxonomy } from 'app/entities/competency.model'; +import { CompetencyService } from 'app/course/competencies/competency.service'; +import { CompetencyRingsComponent } from 'app/course/competencies/competency-rings/competency-rings.component'; +import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { NgbTooltipMocksModule } from '../../../../helpers/mocks/directive/ngbTooltipMocks.module'; + +describe('CompetencyNodeDetailsComponent', () => { + let fixture: ComponentFixture; + let comp: CompetencyNodeDetailsComponent; + let competencyService: CompetencyService; + let findByIdStub: jest.SpyInstance; + let competency: Competency; + let competencyProgress: CompetencyProgress; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule, NgbTooltipMocksModule], + declarations: [CompetencyNodeDetailsComponent, MockComponent(CompetencyRingsComponent), MockPipe(ArtemisTranslatePipe)], + providers: [], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(CompetencyNodeDetailsComponent); + comp = fixture.componentInstance; + competency = new Competency(); + competency.id = 2; + competency.title = 'Some arbitrary title'; + competency.description = 'Some description'; + competency.taxonomy = CompetencyTaxonomy.ANALYZE; + competency.masteryThreshold = 50; + competencyProgress = new CompetencyProgress(); + competencyProgress.progress = 80; + competencyProgress.confidence = 70; + competency.userProgress = [competencyProgress]; + + competencyService = TestBed.inject(CompetencyService); + findByIdStub = jest.spyOn(competencyService, 'findById').mockReturnValue(of(new HttpResponse({ body: competency }))); + comp.courseId = 1; + comp.competencyId = competency.id; + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should load competency on init', () => { + fixture.detectChanges(); + expect(findByIdStub).toHaveBeenCalledOnce(); + expect(findByIdStub).toHaveBeenCalledWith(competency.id, 1); + expect(comp.competency).toEqual(competency); + expect(comp.competencyProgress).toEqual(competencyProgress); + }); + + it('should default progress to zero if empty', () => { + competency.userProgress = undefined; + fixture.detectChanges(); + expect(findByIdStub).toHaveBeenCalledOnce(); + expect(findByIdStub).toHaveBeenCalledWith(competency.id, 1); + expect(comp.competency).toEqual(competency); + expect(comp.competencyProgress).toEqual({ confidence: 0, progress: 0 } as CompetencyProgress); + }); +}); diff --git a/src/test/javascript/spec/component/learning-paths/graph/node-details/exercise-node-details.component.spec.ts b/src/test/javascript/spec/component/learning-paths/graph/node-details/exercise-node-details.component.spec.ts new file mode 100644 index 000000000000..c1bc0d981769 --- /dev/null +++ b/src/test/javascript/spec/component/learning-paths/graph/node-details/exercise-node-details.component.spec.ts @@ -0,0 +1,52 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ArtemisTestModule } from '../../../../test.module'; +import { MockComponent, MockPipe } from 'ng-mocks'; +import { of } from 'rxjs'; +import { HttpResponse } from '@angular/common/http'; +import { CompetencyNodeDetailsComponent } from 'app/course/learning-paths/learning-path-graph/node-details/competency-node-details.component'; +import { Competency, CompetencyProgress, CompetencyTaxonomy } from 'app/entities/competency.model'; +import { CompetencyService } from 'app/course/competencies/competency.service'; +import { CompetencyRingsComponent } from 'app/course/competencies/competency-rings/competency-rings.component'; +import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { NgbTooltipMocksModule } from '../../../../helpers/mocks/directive/ngbTooltipMocks.module'; +import { ExerciseNodeDetailsComponent } from 'app/course/learning-paths/learning-path-graph/node-details/exercise-node-details.component'; +import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; +import { Exercise } from 'app/entities/exercise.model'; + +describe('ExerciseNodeDetailsComponent', () => { + let fixture: ComponentFixture; + let comp: ExerciseNodeDetailsComponent; + let exerciseService: ExerciseService; + let findStub: jest.SpyInstance; + let exercise: Exercise; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule, NgbTooltipMocksModule], + declarations: [ExerciseNodeDetailsComponent], + providers: [], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(ExerciseNodeDetailsComponent); + comp = fixture.componentInstance; + exercise = new Exercise(); + exercise.id = 1; + + exerciseService = TestBed.inject(ExerciseService); + findStub = jest.spyOn(exerciseService, 'find').mockReturnValue(of(new HttpResponse({ body: exercise }))); + comp.exerciseId = exercise.id; + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should load exercise on init', () => { + fixture.detectChanges(); + expect(findStub).toHaveBeenCalledOnce(); + expect(findStub).toHaveBeenCalledWith(exercise.id); + expect(comp.exercise).toEqual(exercise); + }); +}); diff --git a/src/test/javascript/spec/component/learning-paths/graph/node-details/lecture-unit-node-details.component.spec.ts b/src/test/javascript/spec/component/learning-paths/graph/node-details/lecture-unit-node-details.component.spec.ts new file mode 100644 index 000000000000..0c7a44364c97 --- /dev/null +++ b/src/test/javascript/spec/component/learning-paths/graph/node-details/lecture-unit-node-details.component.spec.ts @@ -0,0 +1,57 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ArtemisTestModule } from '../../../../test.module'; +import { MockPipe } from 'ng-mocks'; +import { of } from 'rxjs'; +import { HttpResponse } from '@angular/common/http'; +import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { NgbTooltipMocksModule } from '../../../../helpers/mocks/directive/ngbTooltipMocks.module'; +import { LectureUnitNodeDetailsComponent } from 'app/course/learning-paths/learning-path-graph/node-details/lecture-unit-node-details.component'; +import { LectureService } from 'app/lecture/lecture.service'; +import { Lecture } from 'app/entities/lecture.model'; +import { LectureUnit } from 'app/entities/lecture-unit/lectureUnit.model'; +import { TextUnit } from 'app/entities/lecture-unit/textUnit.model'; + +describe('LectureUnitNodeDetailsComponent', () => { + let fixture: ComponentFixture; + let comp: LectureUnitNodeDetailsComponent; + let lectureService: LectureService; + let findWithDetailsStub: jest.SpyInstance; + let lecture: Lecture; + let lectureUnit: LectureUnit; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule, NgbTooltipMocksModule], + declarations: [LectureUnitNodeDetailsComponent, MockPipe(ArtemisTranslatePipe)], + providers: [], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(LectureUnitNodeDetailsComponent); + comp = fixture.componentInstance; + lecture = new Lecture(); + lecture.id = 1; + lectureUnit = new TextUnit(); + lectureUnit.id = 2; + lectureUnit.name = 'Some arbitrary name'; + lecture.lectureUnits = [lectureUnit]; + + lectureService = TestBed.inject(LectureService); + findWithDetailsStub = jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(of(new HttpResponse({ body: lecture }))); + comp.lectureId = lecture.id; + comp.lectureUnitId = lectureUnit.id; + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should load lecture unit on init', () => { + fixture.detectChanges(); + expect(findWithDetailsStub).toHaveBeenCalledOnce(); + expect(findWithDetailsStub).toHaveBeenCalledWith(lecture.id); + expect(comp.lecture).toEqual(lecture); + expect(comp.lectureUnit).toEqual(lectureUnit); + }); +}); diff --git a/src/test/javascript/spec/component/learning-paths/management/learning-path-progress-modal.component.spec.ts b/src/test/javascript/spec/component/learning-paths/management/learning-path-progress-modal.component.spec.ts index 69044986dbba..aeb8130b5d9a 100644 --- a/src/test/javascript/spec/component/learning-paths/management/learning-path-progress-modal.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/management/learning-path-progress-modal.component.spec.ts @@ -34,6 +34,7 @@ describe('LearningPathProgressModalComponent', () => { it('should display learning path graph if id is present', () => { comp.learningPathId = 1; + comp.courseId = 2; fixture.detectChanges(); expect(fixture.debugElement.query(By.css('.graph')).nativeElement).toBeTruthy(); }); diff --git a/src/test/javascript/spec/directive/sticky-popover.directive.spec.ts b/src/test/javascript/spec/directive/sticky-popover.directive.spec.ts new file mode 100644 index 000000000000..13262c4fcf9a --- /dev/null +++ b/src/test/javascript/spec/directive/sticky-popover.directive.spec.ts @@ -0,0 +1,60 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { Component, DebugElement } from '@angular/core'; +import { ArtemisTestModule } from '../test.module'; +import { By } from '@angular/platform-browser'; +import { StickyPopoverDirective } from 'app/shared/sticky-popover/sticky-popover.directive'; + +@Component({ + template: '
    some content', +}) +class StickyPopoverComponent { + pattern: string; +} + +describe('StickyPopoverDirective', () => { + let fixture: ComponentFixture; + let debugDirective: DebugElement; + let directive: StickyPopoverDirective; + let openStub: jest.SpyInstance; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule], + declarations: [StickyPopoverDirective, StickyPopoverComponent], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(StickyPopoverComponent); + debugDirective = fixture.debugElement.query(By.directive(StickyPopoverDirective)); + directive = debugDirective.injector.get(StickyPopoverDirective); + openStub = jest.spyOn(directive, 'open'); + fixture.detectChanges(); + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should open on hover', fakeAsync(() => { + fixture.whenStable(); + const div = fixture.debugElement.query(By.css('div')); + expect(div).not.toBeNull(); + div.nativeElement.dispatchEvent(new MouseEvent('mouseenter')); + tick(10); + expect(openStub).toHaveBeenCalledOnce(); + expect(directive.isOpen()).toBeTruthy(); + const span = fixture.debugElement.query(By.css('span')); + expect(span).not.toBeNull(); + })); + + it('should display content on hover', fakeAsync(() => { + fixture.whenStable(); + const div = fixture.debugElement.query(By.css('div')); + expect(div).not.toBeNull(); + div.nativeElement.dispatchEvent(new MouseEvent('mouseenter')); + tick(10); + const span = fixture.debugElement.query(By.css('span')); + expect(span).not.toBeNull(); + })); +}); From 0e8d62b2b66f2cfa276ae32bd5995607baea0b44 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 27 Jul 2023 13:40:07 +0200 Subject: [PATCH 077/215] Add exercise node details --- .../exercise-node-details.component.html | 14 ++++++++++++++ .../exercise-node-details.component.ts | 5 ++++- .../exercise-node-details.component.spec.ts | 12 +++++------- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/exercise-node-details.component.html b/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/exercise-node-details.component.html index e69de29bb2d1..d364b09a85b2 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/exercise-node-details.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/exercise-node-details.component.html @@ -0,0 +1,14 @@ +
    +
    +

    + + {{ exercise.title }} +

    +
    +
    diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/exercise-node-details.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/exercise-node-details.component.ts index 5f081d966294..8f265a38fadf 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/exercise-node-details.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/exercise-node-details.component.ts @@ -2,7 +2,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { HttpErrorResponse } from '@angular/common/http'; import { onError } from 'app/shared/util/global.utils'; import { AlertService } from 'app/core/util/alert.service'; -import { Exercise } from 'app/entities/exercise.model'; +import { Exercise, getIcon, getIconTooltip } from 'app/entities/exercise.model'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; @Component({ @@ -32,4 +32,7 @@ export class ExerciseNodeDetailsComponent implements OnInit { error: (errorResponse: HttpErrorResponse) => onError(this.alertService, errorResponse), }); } + + protected readonly getIcon = getIcon; + protected readonly getIconTooltip = getIconTooltip; } diff --git a/src/test/javascript/spec/component/learning-paths/graph/node-details/exercise-node-details.component.spec.ts b/src/test/javascript/spec/component/learning-paths/graph/node-details/exercise-node-details.component.spec.ts index c1bc0d981769..57141684de8a 100644 --- a/src/test/javascript/spec/component/learning-paths/graph/node-details/exercise-node-details.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/graph/node-details/exercise-node-details.component.spec.ts @@ -1,17 +1,14 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ArtemisTestModule } from '../../../../test.module'; -import { MockComponent, MockPipe } from 'ng-mocks'; +import { MockPipe } from 'ng-mocks'; import { of } from 'rxjs'; import { HttpResponse } from '@angular/common/http'; -import { CompetencyNodeDetailsComponent } from 'app/course/learning-paths/learning-path-graph/node-details/competency-node-details.component'; -import { Competency, CompetencyProgress, CompetencyTaxonomy } from 'app/entities/competency.model'; -import { CompetencyService } from 'app/course/competencies/competency.service'; -import { CompetencyRingsComponent } from 'app/course/competencies/competency-rings/competency-rings.component'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { NgbTooltipMocksModule } from '../../../../helpers/mocks/directive/ngbTooltipMocks.module'; import { ExerciseNodeDetailsComponent } from 'app/course/learning-paths/learning-path-graph/node-details/exercise-node-details.component'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { Exercise } from 'app/entities/exercise.model'; +import { TextExercise } from 'app/entities/text-exercise.model'; describe('ExerciseNodeDetailsComponent', () => { let fixture: ComponentFixture; @@ -23,15 +20,16 @@ describe('ExerciseNodeDetailsComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [ArtemisTestModule, NgbTooltipMocksModule], - declarations: [ExerciseNodeDetailsComponent], + declarations: [ExerciseNodeDetailsComponent, MockPipe(ArtemisTranslatePipe)], providers: [], }) .compileComponents() .then(() => { fixture = TestBed.createComponent(ExerciseNodeDetailsComponent); comp = fixture.componentInstance; - exercise = new Exercise(); + exercise = new TextExercise(); exercise.id = 1; + exercise.title = 'Some arbitrary title'; exerciseService = TestBed.inject(ExerciseService); findStub = jest.spyOn(exerciseService, 'find').mockReturnValue(of(new HttpResponse({ body: exercise }))); From 575fcba11d4256c0e89882ee0cceceb962cccd53 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 27 Jul 2023 14:09:59 +0200 Subject: [PATCH 078/215] Fix enableLearningPaths test --- ...learning-path-management.component.spec.ts | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts b/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts index 78f4b25e8e6b..e618d4f3e3f8 100644 --- a/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts @@ -15,8 +15,8 @@ import { CourseManagementService } from 'app/course/manage/course-management.ser import { Course } from 'app/entities/course.model'; import { ActivatedRoute } from '@angular/router'; import { HttpResponse } from '@angular/common/http'; -import { By } from '@angular/platform-browser'; import { LearningPathService } from 'app/course/learning-paths/learning-path.service'; +import { expectElementToBeDisabled } from '../../../helpers/utils/general.utils'; describe('LearningPathManagementComponent', () => { let fixture: ComponentFixture; @@ -88,6 +88,7 @@ describe('LearningPathManagementComponent', () => { ...searchResult, }; searchForLearningPathsStub.mockReturnValue(of(searchResult)); + enableLearningPathsStub.mockReturnValue(of(new HttpResponse())); }); const setStateAndCallOnInit = (middleExpectation: () => void) => { @@ -108,22 +109,19 @@ describe('LearningPathManagementComponent', () => { }); })); - it('should allow to enable learning paths if learning paths disabled', () => { - course.learningPathsEnabled = false; - findCourseStub.mockReturnValue(of(new HttpResponse({ body: course }))); + it('should enable learning paths and load data', fakeAsync(() => { + const disabledCourse = Object.assign({}, course); + disabledCourse.learningPathsEnabled = false; + findCourseStub.mockReturnValueOnce(of(new HttpResponse({ body: disabledCourse }))).mockReturnValueOnce(course); + const loadDataStub = jest.spyOn(comp, 'loadData'); fixture.detectChanges(); comp.ngOnInit(); - expect(comp.course).toEqual(course); - expect(comp.course.learningPathsEnabled).toBeFalsy(); - expect(comp.isLoading).toBeFalsy(); - const button = fixture.debugElement.query(By.css('div')); - console.log(fixture.debugElement); - expect(button).not.toBeNull(); - - button.nativeElement.click(); + comp.enableLearningPaths(); expect(enableLearningPathsStub).toHaveBeenCalledOnce(); expect(enableLearningPathsStub).toHaveBeenCalledWith(course.id); - }); + expect(loadDataStub).toHaveBeenCalledTimes(2); + expect(comp.course.learningPathsEnabled).toBeTruthy(); + })); it('should set content to paging result on sort', fakeAsync(() => { expect(comp.listSorting).toBeTrue(); From 3ecd4dc66789a773e83cecc03b6f940e1d83a7da Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 27 Jul 2023 14:33:34 +0200 Subject: [PATCH 079/215] Fix build fail --- .../management/learning-path-management.component.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts b/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts index e618d4f3e3f8..0300847c271e 100644 --- a/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts @@ -113,13 +113,12 @@ describe('LearningPathManagementComponent', () => { const disabledCourse = Object.assign({}, course); disabledCourse.learningPathsEnabled = false; findCourseStub.mockReturnValueOnce(of(new HttpResponse({ body: disabledCourse }))).mockReturnValueOnce(course); - const loadDataStub = jest.spyOn(comp, 'loadData'); fixture.detectChanges(); comp.ngOnInit(); comp.enableLearningPaths(); expect(enableLearningPathsStub).toHaveBeenCalledOnce(); expect(enableLearningPathsStub).toHaveBeenCalledWith(course.id); - expect(loadDataStub).toHaveBeenCalledTimes(2); + expect(findCourseStub).toHaveBeenCalledTimes(3); expect(comp.course.learningPathsEnabled).toBeTruthy(); })); From 167d50cc255baf4a163eee40fab3018085f058cc Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Fri, 28 Jul 2023 12:12:48 +0200 Subject: [PATCH 080/215] Remove unused Imports --- .../learning-path-progress-modal.component.ts | 2 +- .../management/learning-path-management.component.spec.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.ts index 80fb732c23d3..c898cfde1f28 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit, ViewChild } from '@angular/core'; +import { Component, Input, ViewChild } from '@angular/core'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { LearningPathGraphComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph.component'; @Component({ diff --git a/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts b/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts index 0300847c271e..be735930ae59 100644 --- a/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts @@ -16,7 +16,6 @@ import { Course } from 'app/entities/course.model'; import { ActivatedRoute } from '@angular/router'; import { HttpResponse } from '@angular/common/http'; import { LearningPathService } from 'app/course/learning-paths/learning-path.service'; -import { expectElementToBeDisabled } from '../../../helpers/utils/general.utils'; describe('LearningPathManagementComponent', () => { let fixture: ComponentFixture; From 85a4d85fb03e23d815a5ab200a97a7240c01a028 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Fri, 28 Jul 2023 12:17:48 +0200 Subject: [PATCH 081/215] Remove unused import --- .../webapp/app/course/learning-paths/learning-path.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/webapp/app/course/learning-paths/learning-path.service.ts b/src/main/webapp/app/course/learning-paths/learning-path.service.ts index d20be322b513..5f2f66d54d8d 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path.service.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { HttpClient, HttpResponse } from '@angular/common/http'; -import { Course } from 'app/entities/course.model'; import { LearningPathRecommendationDTO, NgxLearningPathDTO } from 'app/entities/competency/learning-path.model'; import { map } from 'rxjs/operators'; From 6beb5bc9f500ac20e448d57aba47e6b23c63f169 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Fri, 28 Jul 2023 12:41:43 +0200 Subject: [PATCH 082/215] Fix popover width --- .../learning-path-graph-node.component.html | 6 +++--- .../learning-path-graph/learning-path-graph.component.scss | 5 +++++ .../node-details/competency-node-details.component.html | 4 ++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.html b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.html index ba069046e5d7..99e15a52615d 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.html @@ -27,10 +27,10 @@ - - + + - + diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.scss b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.scss index 8b931cbdf212..443b23c9e23f 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.scss +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.scss @@ -35,3 +35,8 @@ jhi-learning-path-graph-node:hover { .completed { color: var(--bs-success); } + +.node-details { + display: block; + max-width: 90vh; +} diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/competency-node-details.component.html b/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/competency-node-details.component.html index b84a4cc2cfcf..78bc30b342fa 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/competency-node-details.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/competency-node-details.component.html @@ -1,5 +1,5 @@
    -
    +

    {{ competency.description }}
    -
    +
    From b8c4e771b04a64fac2cf377a3806be97734b5e2d Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Fri, 28 Jul 2023 13:03:03 +0200 Subject: [PATCH 083/215] fix graph-container overflow --- .../learning-path-graph/learning-path-graph.component.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.scss b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.scss index 443b23c9e23f..9ee5c9bb5f03 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.scss +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.scss @@ -2,6 +2,7 @@ display: block; width: 100%; height: 100%; + overflow: hidden; .ngx-graph { width: auto; From 19fc5251941d51a58ef631017c419f829d232375 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Fri, 28 Jul 2023 14:10:01 +0200 Subject: [PATCH 084/215] fix node-details and navigations via graph --- .../artemis/service/LearningPathService.java | 2 +- .../dto/competency/NgxLearningPathDTO.java | 6 +++++- .../learning-path-graph-node.component.html | 7 ++++++- .../lecture-unit-node-details.component.html | 4 ++-- .../lecture-unit-node-details.component.ts | 5 ++++- .../learning-path-container.component.html | 7 ++++++- .../learning-path-container.component.ts | 20 +++++++++++++++++- ...learning-path-graph-sidebar.component.html | 8 ++++++- .../learning-path-graph-sidebar.component.ts | 6 +++++- .../competency/learning-path.model.ts | 1 + .../lecture-unit/lectureUnit.model.ts | 21 +++++++++---------- .../service/LearningPathServiceTest.java | 9 ++++---- 12 files changed, 71 insertions(+), 25 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index 9a2fc5ca4922..8df97477a170 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -213,7 +213,7 @@ private void generateNgxRepresentationForCompetency(LearningPath learningPath, C // generate nodes and edges for lecture units competency.getLectureUnits().forEach(lectureUnit -> { currentCluster.add(new NgxLearningPathDTO.Node(getLectureUnitNodeId(competency.getId(), lectureUnit.getId()), NgxLearningPathDTO.NodeType.LECTURE_UNIT, - lectureUnit.getId(), lectureUnit.isCompletedFor(learningPath.getUser()), lectureUnit.getName())); + lectureUnit.getId(), lectureUnit.getLecture().getId(), lectureUnit.isCompletedFor(learningPath.getUser()), lectureUnit.getName())); edges.add(new NgxLearningPathDTO.Edge(getLectureUnitInEdgeId(competency.getId(), lectureUnit.getId()), startNodeId, getLectureUnitNodeId(competency.getId(), lectureUnit.getId()))); edges.add(new NgxLearningPathDTO.Edge(getLectureUnitOutEdgeId(competency.getId(), lectureUnit.getId()), getLectureUnitNodeId(competency.getId(), lectureUnit.getId()), diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/NgxLearningPathDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/NgxLearningPathDTO.java index 46c16afc2559..fa676c14a702 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/NgxLearningPathDTO.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/NgxLearningPathDTO.java @@ -10,7 +10,11 @@ @JsonInclude(JsonInclude.Include.NON_EMPTY) public record NgxLearningPathDTO(Set nodes, Set edges, Set clusters) { - public record Node(String id, NodeType type, long linkedResource, boolean completed, String label) { + public record Node(String id, NodeType type, long linkedResource, long linkedResourceParent, boolean completed, String label) { + + public Node(String id, NodeType type, long linkedResource, boolean completed, String label) { + this(id, type, linkedResource, -1, completed, label); + } public Node(String id, NodeType type, long linkedResource, String label) { this(id, type, linkedResource, false, label); diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.html b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.html index 99e15a52615d..5d537b33656e 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph-node.component.html @@ -28,7 +28,12 @@ - + diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/lecture-unit-node-details.component.html b/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/lecture-unit-node-details.component.html index fe46e533a422..4f6553e2ef8e 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/lecture-unit-node-details.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/lecture-unit-node-details.component.html @@ -3,9 +3,9 @@

    {{ lectureUnit.name }} diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/lecture-unit-node-details.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/lecture-unit-node-details.component.ts index 7c8a5245b9d2..97aa66e1c311 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/lecture-unit-node-details.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/lecture-unit-node-details.component.ts @@ -4,7 +4,7 @@ import { onError } from 'app/shared/util/global.utils'; import { AlertService } from 'app/core/util/alert.service'; import { LectureService } from 'app/lecture/lecture.service'; import { Lecture } from 'app/entities/lecture.model'; -import { LectureUnit } from 'app/entities/lecture-unit/lectureUnit.model'; +import { LectureUnit, getIcon, getIconTooltip } from 'app/entities/lecture-unit/lectureUnit.model'; @Component({ selector: 'jhi-lecture-unit-node-details', @@ -39,4 +39,7 @@ export class LectureUnitNodeDetailsComponent implements OnInit { error: (errorResponse: HttpErrorResponse) => onError(this.alertService, errorResponse), }); } + + protected readonly getIcon = getIcon; + protected readonly getIconTooltip = getIconTooltip; } diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html index 8c24c96375eb..e8d240d59786 100644 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html @@ -1,6 +1,11 @@
    - +
    diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts index ea0756b434d1..999f1c52d27a 100644 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts @@ -5,7 +5,7 @@ import { Exercise } from 'app/entities/exercise.model'; import { LectureUnit } from 'app/entities/lecture-unit/lectureUnit.model'; import { Lecture } from 'app/entities/lecture.model'; import { LearningPathService } from 'app/course/learning-paths/learning-path.service'; -import { RecommendationType } from 'app/entities/competency/learning-path.model'; +import { NgxLearningPathNode, NodeType, RecommendationType } from 'app/entities/competency/learning-path.model'; import { LectureService } from 'app/lecture/lecture.service'; import { onError } from 'app/shared/util/global.utils'; import { HttpErrorResponse } from '@angular/common/http'; @@ -143,4 +143,22 @@ export class LearningPathContainerComponent implements OnInit { instance.exerciseId = this.learningObjectId; } } + + onNodeClicked(node: NgxLearningPathNode) { + if (node.type === NodeType.LECTURE_UNIT || node.type === NodeType.EXERCISE) { + if (this.lectureUnit?.id) { + this.history.push([this.lectureUnit.id, this.lectureId!]); + } else if (this.exercise?.id) { + this.history.push([this.exercise.id, -1]); + } + this.undefineAll(); + this.learningObjectId = node.linkedResource!; + this.lectureId = node.linkedResourceParent; + if (node.type === NodeType.LECTURE_UNIT) { + this.loadLectureUnit(); + } else if (node.type === NodeType.EXERCISE) { + this.loadExercise(); + } + } + } } diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.html b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.html index 08a8a91621f6..0c64d339f083 100644 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.html +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.html @@ -13,7 +13,13 @@

    - +
    diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.ts b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.ts index cf6ff5060559..68cc20c26bcf 100644 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.ts +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.ts @@ -1,7 +1,8 @@ -import { AfterViewInit, Component, Input, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; import interact from 'interactjs'; import { faChevronLeft, faChevronRight, faGripLinesVertical, faNetworkWired } from '@fortawesome/free-solid-svg-icons'; import { LearningPathGraphComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph.component'; +import { NgxLearningPathNode } from 'app/entities/competency/learning-path.model'; @Component({ selector: 'jhi-learning-path-graph-sidebar', @@ -9,6 +10,7 @@ import { LearningPathGraphComponent } from 'app/course/learning-paths/learning-p templateUrl: './learning-path-graph-sidebar.component.html', }) export class LearningPathGraphSidebarComponent implements AfterViewInit { + @Input() courseId: number; @Input() learningPathId: number; collapsed: boolean; // Icons @@ -20,6 +22,8 @@ export class LearningPathGraphSidebarComponent implements AfterViewInit { @ViewChild(`learningPathGraphComponent`, { static: false }) learningPathGraphComponent: LearningPathGraphComponent; + @Output() nodeClicked: EventEmitter = new EventEmitter(); + ngAfterViewInit(): void { // allows the conversation sidebar to be resized towards the right-hand side interact('.expanded-graph') diff --git a/src/main/webapp/app/entities/competency/learning-path.model.ts b/src/main/webapp/app/entities/competency/learning-path.model.ts index 018af1edf2b7..7cee6e4ad2e4 100644 --- a/src/main/webapp/app/entities/competency/learning-path.model.ts +++ b/src/main/webapp/app/entities/competency/learning-path.model.ts @@ -30,6 +30,7 @@ export class NgxLearningPathNode implements Node { public id: string; public type?: NodeType; public linkedResource?: number; + public linkedResourceParent?: number; public completed?: boolean; public label?: string; } diff --git a/src/main/webapp/app/entities/lecture-unit/lectureUnit.model.ts b/src/main/webapp/app/entities/lecture-unit/lectureUnit.model.ts index e47782e08fee..f525b0753661 100644 --- a/src/main/webapp/app/entities/lecture-unit/lectureUnit.model.ts +++ b/src/main/webapp/app/entities/lecture-unit/lectureUnit.model.ts @@ -45,19 +45,18 @@ export abstract class LectureUnit implements BaseEntity { protected constructor(type: LectureUnitType) { this.type = type; } +} - get getIcon(): IconProp { - if (!this.type) { - return faQuestion as IconProp; - } - return lectureUnitIcons[this.type] as IconProp; +export function getIcon(lectureUnitType: LectureUnitType): IconProp { + if (!lectureUnitType) { + return faQuestion as IconProp; } + return lectureUnitIcons[lectureUnitType] as IconProp; +} - getIconTooltip(): string { - if (!this.type) { - return ''; - } - - return lectureUnitTooltips[this.type]; +export function getIconTooltip(lectureUnitType: LectureUnitType) { + if (!lectureUnitType) { + return ''; } + return lectureUnitTooltips[lectureUnitType]; } diff --git a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java index 0fc4c8e02c88..dabe91025d79 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java @@ -2,9 +2,8 @@ import static org.assertj.core.api.Assertions.assertThat; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; +import java.time.ZonedDateTime; +import java.util.*; import java.util.stream.Collectors; import org.junit.jupiter.api.BeforeEach; @@ -107,7 +106,9 @@ void testEmptyCompetency() { @Test void testCompetencyWithLectureUnitAndExercise() { var competency = competencyUtilService.createCompetency(course); + var lecture = lectureUtilService.createLecture(course, ZonedDateTime.now()); final var lectureUnit = lectureUtilService.createTextUnit(); + lectureUtilService.addLectureUnitsToLecture(lecture, List.of(lectureUnit)); competencyUtilService.linkLectureUnitToCompetency(competency, lectureUnit); final var exercise = programmingExerciseUtilService.addProgrammingExerciseToCourse(course, false, false, ProgrammingLanguage.JAVA, "Some Title", "someshortname"); competencyUtilService.linkExerciseToCompetency(competency, exercise); @@ -263,7 +264,7 @@ private static Set getExpectedNodesOfEmptyCompetency(Co private static NgxLearningPathDTO.Node getNodeForLectureUnit(Competency competency, LectureUnit lectureUnit) { return new NgxLearningPathDTO.Node(LearningPathService.getLectureUnitNodeId(competency.getId(), lectureUnit.getId()), NgxLearningPathDTO.NodeType.LECTURE_UNIT, - lectureUnit.getId(), lectureUnit.getName()); + lectureUnit.getId(), lectureUnit.getLecture().getId(), false, lectureUnit.getName()); } private static NgxLearningPathDTO.Node getNodeForExercise(Competency competency, Exercise exercise) { From b958d7cd5af3e5cd43c7ec36095c00bb926bb437 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Fri, 28 Jul 2023 14:56:38 +0200 Subject: [PATCH 085/215] add tests for node click event in graph container --- .../learning-path-container.component.spec.ts | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/test/javascript/spec/component/learning-paths/participate/learning-path-container.component.spec.ts b/src/test/javascript/spec/component/learning-paths/participate/learning-path-container.component.spec.ts index bc952252b548..e9d2e74ca2ce 100644 --- a/src/test/javascript/spec/component/learning-paths/participate/learning-path-container.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/participate/learning-path-container.component.spec.ts @@ -7,7 +7,7 @@ import { ActivatedRoute, RouterModule } from '@angular/router'; import { HttpResponse } from '@angular/common/http'; import { LearningPathContainerComponent } from 'app/course/learning-paths/participate/learning-path-container.component'; import { LearningPathService } from 'app/course/learning-paths/learning-path.service'; -import { LearningPathRecommendationDTO, RecommendationType } from 'app/entities/competency/learning-path.model'; +import { LearningPathRecommendationDTO, NgxLearningPathNode, NodeType, RecommendationType } from 'app/entities/competency/learning-path.model'; import { LectureService } from 'app/lecture/lecture.service'; import { Lecture } from 'app/entities/lecture.model'; import { LectureUnit } from 'app/entities/lecture-unit/lectureUnit.model'; @@ -180,4 +180,39 @@ describe('LearningPathContainerComponent', () => { expect(instance.courseId).toBe(1); expect(instance.exerciseId).toEqual(exercise.id); }); + + it('should handle lecture unit node click', () => { + const node = { id: 1, type: NodeType.LECTURE_UNIT, linkedResource: 2, linkedResourceParent: 3 } as NgxLearningPathNode; + comp.onNodeClicked(node); + expect(comp.learningObjectId).toBe(node.linkedResource); + expect(comp.lectureId).toBe(node.linkedResourceParent); + expect(findWithDetailsStub).toHaveBeenCalledWith(node.linkedResourceParent); + }); + + it('should handle exercise node click', () => { + const node = { id: 1, type: NodeType.EXERCISE, linkedResource: 2 } as NgxLearningPathNode; + comp.onNodeClicked(node); + expect(comp.learningObjectId).toBe(node.linkedResource); + expect(getExerciseDetailsStub).toHaveBeenCalledWith(node.linkedResource); + }); + + it('should handle store current lecture unit in history on node click', () => { + comp.learningObjectId = lectureUnit.id!; + comp.lectureUnit = lectureUnit; + comp.lectureId = lecture.id; + comp.lecture = lecture; + fixture.detectChanges(); + const node = { id: 1, type: NodeType.EXERCISE, linkedResource: 2 } as NgxLearningPathNode; + comp.onNodeClicked(node); + expect(comp.history).toEqual([[lectureUnit.id!, lecture.id!]]); + }); + + it('should handle store current exercise in history on node click', () => { + comp.learningObjectId = exercise.id!; + comp.exercise = exercise; + fixture.detectChanges(); + const node = { id: 1, type: NodeType.EXERCISE, linkedResource: 2 } as NgxLearningPathNode; + comp.onNodeClicked(node); + expect(comp.history).toEqual([[exercise.id!, -1]]); + }); }); From d7fb00743f792614cb8933be677a8b4d0e6a0238 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Fri, 28 Jul 2023 15:03:34 +0200 Subject: [PATCH 086/215] fix compiler error --- .../node-details/exercise-node-details.component.spec.ts | 2 +- .../participate/learning-path-container.component.spec.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/javascript/spec/component/learning-paths/graph/node-details/exercise-node-details.component.spec.ts b/src/test/javascript/spec/component/learning-paths/graph/node-details/exercise-node-details.component.spec.ts index 57141684de8a..839aeccf50de 100644 --- a/src/test/javascript/spec/component/learning-paths/graph/node-details/exercise-node-details.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/graph/node-details/exercise-node-details.component.spec.ts @@ -27,7 +27,7 @@ describe('ExerciseNodeDetailsComponent', () => { .then(() => { fixture = TestBed.createComponent(ExerciseNodeDetailsComponent); comp = fixture.componentInstance; - exercise = new TextExercise(); + exercise = new TextExercise(undefined, undefined); exercise.id = 1; exercise.title = 'Some arbitrary title'; diff --git a/src/test/javascript/spec/component/learning-paths/participate/learning-path-container.component.spec.ts b/src/test/javascript/spec/component/learning-paths/participate/learning-path-container.component.spec.ts index e9d2e74ca2ce..d0ec5a7fd897 100644 --- a/src/test/javascript/spec/component/learning-paths/participate/learning-path-container.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/participate/learning-path-container.component.spec.ts @@ -182,7 +182,7 @@ describe('LearningPathContainerComponent', () => { }); it('should handle lecture unit node click', () => { - const node = { id: 1, type: NodeType.LECTURE_UNIT, linkedResource: 2, linkedResourceParent: 3 } as NgxLearningPathNode; + const node = { id: 'some-id', type: NodeType.LECTURE_UNIT, linkedResource: 2, linkedResourceParent: 3 } as NgxLearningPathNode; comp.onNodeClicked(node); expect(comp.learningObjectId).toBe(node.linkedResource); expect(comp.lectureId).toBe(node.linkedResourceParent); @@ -190,7 +190,7 @@ describe('LearningPathContainerComponent', () => { }); it('should handle exercise node click', () => { - const node = { id: 1, type: NodeType.EXERCISE, linkedResource: 2 } as NgxLearningPathNode; + const node = { id: 'some-id', type: NodeType.EXERCISE, linkedResource: 2 } as NgxLearningPathNode; comp.onNodeClicked(node); expect(comp.learningObjectId).toBe(node.linkedResource); expect(getExerciseDetailsStub).toHaveBeenCalledWith(node.linkedResource); @@ -202,7 +202,7 @@ describe('LearningPathContainerComponent', () => { comp.lectureId = lecture.id; comp.lecture = lecture; fixture.detectChanges(); - const node = { id: 1, type: NodeType.EXERCISE, linkedResource: 2 } as NgxLearningPathNode; + const node = { id: 'some-id', type: NodeType.EXERCISE, linkedResource: 2 } as NgxLearningPathNode; comp.onNodeClicked(node); expect(comp.history).toEqual([[lectureUnit.id!, lecture.id!]]); }); @@ -211,7 +211,7 @@ describe('LearningPathContainerComponent', () => { comp.learningObjectId = exercise.id!; comp.exercise = exercise; fixture.detectChanges(); - const node = { id: 1, type: NodeType.EXERCISE, linkedResource: 2 } as NgxLearningPathNode; + const node = { id: 'some-id', type: NodeType.EXERCISE, linkedResource: 2 } as NgxLearningPathNode; comp.onNodeClicked(node); expect(comp.history).toEqual([[exercise.id!, -1]]); }); From bc131e3043d1621c86db831e8bb3245896c40cea Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Fri, 28 Jul 2023 15:29:05 +0200 Subject: [PATCH 087/215] remove native types from DTO --- .../web/rest/dto/competency/NgxLearningPathDTO.java | 13 ++++++++----- .../artemis/service/LearningPathServiceTest.java | 8 ++++---- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/NgxLearningPathDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/NgxLearningPathDTO.java index 46c16afc2559..749da948dba3 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/NgxLearningPathDTO.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/NgxLearningPathDTO.java @@ -10,28 +10,31 @@ @JsonInclude(JsonInclude.Include.NON_EMPTY) public record NgxLearningPathDTO(Set nodes, Set edges, Set clusters) { - public record Node(String id, NodeType type, long linkedResource, boolean completed, String label) { + @JsonInclude(JsonInclude.Include.NON_NULL) + public record Node(String id, NodeType type, Long linkedResource, boolean completed, String label) { - public Node(String id, NodeType type, long linkedResource, String label) { + public Node(String id, NodeType type, Long linkedResource, String label) { this(id, type, linkedResource, false, label); } public Node(String id, NodeType type, String label) { - this(id, type, -1, label); + this(id, type, null, label); } - public Node(String id, NodeType type, long linkedResource) { + public Node(String id, NodeType type, Long linkedResource) { this(id, type, linkedResource, ""); } public Node(String id, NodeType type) { - this(id, type, -1); + this(id, type, null, ""); } } + @JsonInclude(JsonInclude.Include.NON_NULL) public record Edge(String id, String source, String target) { } + @JsonInclude(JsonInclude.Include.NON_NULL) public record Cluster(String id, String label, Set childNodeIds) { } diff --git a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java index 0fc4c8e02c88..1588ae37b389 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java @@ -205,8 +205,8 @@ void testSingleExtends() { @Test void testSingleMatches() { competencyUtilService.addRelation(competency1, CompetencyRelation.RelationType.MATCHES, competency2); - expectedNodes.add(new NgxLearningPathDTO.Node(LearningPathService.getMatchingClusterStartNodeId(0), NgxLearningPathDTO.NodeType.COMPETENCY_START, -1, "")); - expectedNodes.add(new NgxLearningPathDTO.Node(LearningPathService.getMatchingClusterEndNodeId(0), NgxLearningPathDTO.NodeType.COMPETENCY_END, -1, "")); + expectedNodes.add(new NgxLearningPathDTO.Node(LearningPathService.getMatchingClusterStartNodeId(0), NgxLearningPathDTO.NodeType.COMPETENCY_START, null, "")); + expectedNodes.add(new NgxLearningPathDTO.Node(LearningPathService.getMatchingClusterEndNodeId(0), NgxLearningPathDTO.NodeType.COMPETENCY_END, null, "")); expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getInEdgeId(competency1.getId()), LearningPathService.getMatchingClusterStartNodeId(0), LearningPathService.getCompetencyStartNodeId(competency1.getId()))); expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getOutEdgeId(competency1.getId()), LearningPathService.getCompetencyEndNodeId(competency1.getId()), @@ -226,8 +226,8 @@ void testMatchesTransitive() { competencyUtilService.addRelation(competency1, CompetencyRelation.RelationType.MATCHES, competency2); competencyUtilService.addRelation(competency2, CompetencyRelation.RelationType.MATCHES, competency3); - expectedNodes.add(new NgxLearningPathDTO.Node(LearningPathService.getMatchingClusterStartNodeId(0), NgxLearningPathDTO.NodeType.COMPETENCY_START, -1, "")); - expectedNodes.add(new NgxLearningPathDTO.Node(LearningPathService.getMatchingClusterEndNodeId(0), NgxLearningPathDTO.NodeType.COMPETENCY_END, -1, "")); + expectedNodes.add(new NgxLearningPathDTO.Node(LearningPathService.getMatchingClusterStartNodeId(0), NgxLearningPathDTO.NodeType.COMPETENCY_START, null, "")); + expectedNodes.add(new NgxLearningPathDTO.Node(LearningPathService.getMatchingClusterEndNodeId(0), NgxLearningPathDTO.NodeType.COMPETENCY_END, null, "")); expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getInEdgeId(competency1.getId()), LearningPathService.getMatchingClusterStartNodeId(0), LearningPathService.getCompetencyStartNodeId(competency1.getId()))); expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getOutEdgeId(competency1.getId()), LearningPathService.getCompetencyEndNodeId(competency1.getId()), From 8f9781ec05e7341d81f0419bda18ee2f382e8c1c Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Fri, 28 Jul 2023 16:42:47 +0200 Subject: [PATCH 088/215] minimize transfered data --- .../artemis/repository/CourseRepository.java | 14 ------ .../www1/artemis/web/rest/CourseResource.java | 30 ++++++------ .../learning-path-management.component.html | 4 +- .../learning-path-management.component.ts | 49 +++++++++---------- .../manage/course-management.service.ts | 18 +++---- .../artemis/course/CourseTestService.java | 34 ++++++------- ...rseBitbucketBambooJiraIntegrationTest.java | 4 +- .../CourseGitlabJenkinsIntegrationTest.java | 4 +- .../course/course-management.service.spec.ts | 13 ----- ...learning-path-management.component.spec.ts | 30 +++++------- 10 files changed, 81 insertions(+), 119 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java index 597f44c0f646..14d3251ccdc3 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java @@ -131,15 +131,6 @@ SELECT CASE WHEN (count(c) > 0) THEN true ELSE false END @EntityGraph(type = LOAD, attributePaths = { "competencies", "prerequisites" }) Optional findWithEagerCompetenciesById(long courseId); - @Query(""" - SELECT c - FROM Course c - LEFT JOIN FETCH c.learningPaths lp - LEFT JOIN FETCH lp.user - WHERE c.id = :courseId - """) - Optional findWithEagerLearningPathsById(@Param("courseId") long courseId); - @EntityGraph(type = LOAD, attributePaths = { "competencies", "learningPaths", "learningPaths.competencies" }) Optional findWithEagerLearningPathsAndCompetenciesById(long courseId); @@ -407,11 +398,6 @@ default Course findWithEagerCompetenciesByIdElseThrow(long courseId) { return findWithEagerCompetenciesById(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId)); } - @NotNull - default Course findWithEagerLearningPathsByIdElseThrow(long courseId) { - return findWithEagerLearningPathsById(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId)); - } - @NotNull default Course findWithEagerLearningPathsAndCompetenciesByIdElseThrow(long courseId) { return findWithEagerLearningPathsAndCompetenciesById(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId)); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java index 9d7e0048246d..81ced00f114a 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java @@ -629,21 +629,6 @@ public ResponseEntity getCourseWithOrganizations(@PathVariable Long cour return ResponseEntity.ok(course); } - /** - * GET /courses/:courseId/with-learning-paths Get a course by id with eagerly loaded learning paths - * - * @param courseId the id of the course - * @return the course with eagerly loaded learning paths - */ - @GetMapping("courses/{courseId}/with-learning-paths") - @EnforceAtLeastInstructor - public ResponseEntity getCourseWithLearningPaths(@PathVariable Long courseId) { - log.debug("REST request to get a course with its organizations : {}", courseId); - Course course = courseRepository.findWithEagerLearningPathsByIdElseThrow(courseId); - authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null); - return ResponseEntity.ok(course); - } - /** * GET /courses/:courseId/lockedSubmissions Get locked submissions for course for user * @@ -1219,4 +1204,19 @@ public ResponseEntity> addUsersToCourseGroup(@PathVariable Long List notFoundStudentsDtos = courseService.registerUsersForCourseGroup(courseId, studentDtos, courseGroup); return ResponseEntity.ok().body(notFoundStudentsDtos); } + + /** + * GET /courses/:courseId/learning-paths-enabled Get a course by id with eagerly loaded learning paths + * + * @param courseId the id of the course + * @return the course with eagerly loaded learning paths + */ + @GetMapping("courses/{courseId}/learning-paths-enabled") + @EnforceAtLeastInstructor + public ResponseEntity getCourseLearningPathsEnabled(@PathVariable Long courseId) { + log.debug("REST request to get if course has learning paths enabled : {}", courseId); + Course course = courseRepository.findByIdElseThrow(courseId); + authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null); + return ResponseEntity.ok(course.getLearningPathsEnabled()); + } } diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html index 0bb39b84f150..8ac77b855294 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html @@ -6,7 +6,7 @@

    Learning Path Management

    -
    +
    Disabled
    -
    +
    Search for Learning Path: diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts index 8b838e2f5a0c..04fe47a1f7e3 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts @@ -1,8 +1,7 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { CourseManagementService } from 'app/course/manage/course-management.service'; -import { Course } from 'app/entities/course.model'; -import { Subject, Subscription } from 'rxjs'; +import { Subject } from 'rxjs'; import { LearningPathService } from 'app/course/learning-paths/learning-path.service'; import { debounceTime, finalize, switchMap, tap } from 'rxjs/operators'; import { HttpErrorResponse } from '@angular/common/http'; @@ -27,13 +26,11 @@ export enum TableColumn { selector: 'jhi-learning-path-management', templateUrl: './learning-path-management.component.html', }) -export class LearningPathManagementComponent implements OnInit, OnDestroy { +export class LearningPathManagementComponent implements OnInit { isLoading = false; courseId: number; - course: Course; - - courseSub: Subscription; + learningPathsEnabled: boolean; searchLoading = false; readonly column = TableColumn; @@ -127,25 +124,23 @@ export class LearningPathManagementComponent implements OnInit, OnDestroy { private loadData() { this.isLoading = true; - this.courseSub = this.courseManagementService.findWithLearningPaths(this.courseId).subscribe((courseResponse) => { - this.course = courseResponse.body!; - - if (this.course.learningPathsEnabled) { - this.performSearch(this.sort, 0); - this.performSearch(this.search, 300); - } - - this.isLoading = false; - }); - } - - /** - * On destroy unsubscribe all subscriptions. - */ - ngOnDestroy() { - if (this.courseSub) { - this.courseSub.unsubscribe(); - } + this.courseManagementService + .getCourseLearningPathsEnabled(this.courseId) + .pipe( + finalize(() => { + this.isLoading = false; + }), + ) + .subscribe({ + next: (res) => { + this.learningPathsEnabled = res.body!; + if (this.learningPathsEnabled) { + this.performSearch(this.sort, 0); + this.performSearch(this.search, 300); + } + }, + error: (res: HttpErrorResponse) => onError(this.alertService, res), + }); } enableLearningPaths() { @@ -159,7 +154,7 @@ export class LearningPathManagementComponent implements OnInit, OnDestroy { ) .subscribe({ next: () => { - this.course.learningPathsEnabled = true; + this.learningPathsEnabled = true; }, error: (res: HttpErrorResponse) => onError(this.alertService, res), }); diff --git a/src/main/webapp/app/course/manage/course-management.service.ts b/src/main/webapp/app/course/manage/course-management.service.ts index 15c01e3788ca..5ff5ce30f36d 100644 --- a/src/main/webapp/app/course/manage/course-management.service.ts +++ b/src/main/webapp/app/course/manage/course-management.service.ts @@ -134,16 +134,6 @@ export class CourseManagementService { .pipe(map((res: EntityResponseType) => this.processCourseEntityResponseType(res))); } - /** - * finds a course with the given id and eagerly loaded learning pahts - * @param courseId the id of the course to be found - */ - findWithLearningPaths(courseId: number): Observable { - return this.http - .get(`${this.resourceUrl}/${courseId}/with-learning-paths`, { observe: 'response' }) - .pipe(map((res: EntityResponseType) => this.processCourseEntityResponseType(res))); - } - // TODO: separate course overview and course management REST API calls in a better way /** * finds all courses using a GET request @@ -695,4 +685,12 @@ export class CourseManagementService { course?.exams?.forEach((exam) => this.entityTitleService.setTitle(EntityType.EXAM, [exam.id], exam.title)); course?.organizations?.forEach((org) => this.entityTitleService.setTitle(EntityType.ORGANIZATION, [org.id], org.name)); } + + /** + * retrieves if the course with the given id has enabled learning paths + * @param courseId the id of the course + */ + getCourseLearningPathsEnabled(courseId: number): Observable> { + return this.http.get(`${this.resourceUrl}/${courseId}/learning-paths-enabled`, { observe: 'response' }); + } } diff --git a/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java b/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java index f1be922ba58c..e88e16c8d8f0 100644 --- a/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java +++ b/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java @@ -3133,24 +3133,24 @@ private String getUpdateOnlineCourseConfigurationPath(String courseId) { } // Test - public void testFindWithLearningPaths_AsInstructor() throws Exception { - String testSuffix = "findwithlearningpaths"; + public void testGetCourseLearningPathsEnabled_AsInstructor() throws Exception { + String testSuffix = "getlearningpathsenabled"; adjustUserGroupsToCustomGroups(testSuffix); - var course = courseUtilService.createCourse(); - adjustCourseGroups(course, testSuffix); - course = courseRepo.save(course); - course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); - - final var result = request.get("/api/courses/" + course.getId() + "/with-learning-paths", HttpStatus.OK, Course.class); - - assertThat(result.getLearningPathsEnabled()).isTrue(); - assertThat(result.getLearningPaths().size()).as("all learning paths are returned").isEqualTo(8); - final var anyLearningPath = result.getLearningPaths().stream().findAny().orElseThrow(); - assertThat(anyLearningPath.getUser()).isNotNull(); - final var student = anyLearningPath.getUser(); - assertThat(student.getLogin()).as("associated student has login").isNotEmpty(); - assertThat(student.getFirstName()).as("associated student has first name").isNotEmpty(); - assertThat(student.getLastName()).as("associated student has last name").isNotEmpty(); + var course1 = courseUtilService.createCourse(); + adjustCourseGroups(course1, testSuffix); + course1.setLearningPathsEnabled(true); + course1 = courseRepo.save(course1); + + var course2 = courseUtilService.createCourse(); + adjustCourseGroups(course2, testSuffix); + course2.setLearningPathsEnabled(false); + course2 = courseRepo.save(course2); + + final var result1 = request.get("/api/courses/" + course1.getId() + "/learning-paths-enabled", HttpStatus.OK, Boolean.class); + assertThat(result1).isTrue(); + + final var result2 = request.get("/api/courses/" + course2.getId() + "/learning-paths-enabled", HttpStatus.OK, Boolean.class); + assertThat(result2).isFalse(); } } diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseBitbucketBambooJiraIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseBitbucketBambooJiraIntegrationTest.java index 868a479b00fa..1c24d7cc0243 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseBitbucketBambooJiraIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseBitbucketBambooJiraIntegrationTest.java @@ -921,7 +921,7 @@ void testEditCourseRemoveExistingIcon() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testFindWithLearningPaths_AsInstructor() throws Exception { - courseTestService.testFindWithLearningPaths_AsInstructor(); + void testGetCourseLearningPathsEnabled_AsInstructor() throws Exception { + courseTestService.testGetCourseLearningPathsEnabled_AsInstructor(); } } diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseGitlabJenkinsIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseGitlabJenkinsIntegrationTest.java index 0d2f26c98d12..690edef3a80c 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseGitlabJenkinsIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseGitlabJenkinsIntegrationTest.java @@ -1039,7 +1039,7 @@ void testEditCourseRemoveExistingIcon() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testFindWithLearningPaths_AsInstructor() throws Exception { - courseTestService.testFindWithLearningPaths_AsInstructor(); + void testGetCourseLearningPathsEnabled_AsInstructor() throws Exception { + courseTestService.testGetCourseLearningPathsEnabled_AsInstructor(); } } diff --git a/src/test/javascript/spec/component/course/course-management.service.spec.ts b/src/test/javascript/spec/component/course/course-management.service.spec.ts index 30d2ccb42d81..41d6d0fa6ec2 100644 --- a/src/test/javascript/spec/component/course/course-management.service.spec.ts +++ b/src/test/javascript/spec/component/course/course-management.service.spec.ts @@ -26,7 +26,6 @@ import { CourseScores } from 'app/course/course-scores/course-scores'; import { ScoresStorageService } from 'app/course/course-scores/scores-storage.service'; import { CourseStorageService } from 'app/course/manage/course-storage.service'; import { Result } from 'app/entities/result.model'; -import { LearningPath } from 'app/entities/competency/learning-path.model'; describe('Course Management Service', () => { let courseManagementService: CourseManagementService; @@ -204,18 +203,6 @@ describe('Course Management Service', () => { tick(); })); - it('should find course with learning paths', fakeAsync(() => { - course.learningPathsEnabled = true; - course.learningPaths = [new LearningPath()]; - returnedFromService = { ...course }; - courseManagementService - .findWithLearningPaths(course.id!) - .pipe(take(1)) - .subscribe((res) => expect(res.body).toEqual(course)); - requestAndExpectDateConversion('GET', `${resourceUrl}/${course.id}/with-learning-paths`, returnedFromService, course); - tick(); - })); - it('should find all courses for dashboard', fakeAsync(() => { const courseStorageServiceSpy = jest.spyOn(courseStorageService, 'setCourses'); returnedFromService = [{ ...courseForDashboard }]; diff --git a/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts b/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts index ea127624ba5f..b6d0c0a41332 100644 --- a/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts @@ -12,7 +12,6 @@ import { SortByDirective } from 'app/shared/sort/sort-by.directive'; import { SortDirective } from 'app/shared/sort/sort.directive'; import { of } from 'rxjs'; import { CourseManagementService } from 'app/course/manage/course-management.service'; -import { Course } from 'app/entities/course.model'; import { ActivatedRoute } from '@angular/router'; import { HttpResponse } from '@angular/common/http'; @@ -20,7 +19,7 @@ describe('LearningPathManagementComponent', () => { let fixture: ComponentFixture; let comp: LearningPathManagementComponent; let courseManagementService: CourseManagementService; - let findCourseStub: jest.SpyInstance; + let getCourseLearningPathsEnabledStub: jest.SpyInstance; let pagingService: LearningPathPagingService; let sortService: SortService; let searchForLearningPathsStub: jest.SpyInstance; @@ -28,7 +27,7 @@ describe('LearningPathManagementComponent', () => { let searchResult: SearchResult; let state: PageableSearch; let learningPath: LearningPath; - let course: Course; + let courseId: number; beforeEach(() => { TestBed.configureTestingModule({ imports: [ArtemisTestModule, MockComponent(NgbPagination)], @@ -52,7 +51,7 @@ describe('LearningPathManagementComponent', () => { fixture = TestBed.createComponent(LearningPathManagementComponent); comp = fixture.componentInstance; courseManagementService = TestBed.inject(CourseManagementService); - findCourseStub = jest.spyOn(courseManagementService, 'findWithLearningPaths'); + getCourseLearningPathsEnabledStub = jest.spyOn(courseManagementService, 'getCourseLearningPathsEnabled'); pagingService = TestBed.inject(LearningPathPagingService); sortService = TestBed.inject(SortService); searchForLearningPathsStub = jest.spyOn(pagingService, 'searchForLearningPaths'); @@ -67,12 +66,9 @@ describe('LearningPathManagementComponent', () => { beforeEach(() => { fixture.detectChanges(); learningPath = new LearningPath(); - learningPath.id = 1; - course = new Course(); - course.id = 1; - course.learningPathsEnabled = true; - course.learningPaths = [learningPath]; - findCourseStub.mockReturnValue(of(new HttpResponse({ body: course }))); + learningPath.id = 2; + courseId = 1; + getCourseLearningPathsEnabledStub.mockReturnValue(of(new HttpResponse({ body: true }))); searchResult = { numberOfPages: 3, resultsOnPage: [learningPath] }; state = { page: 1, @@ -94,12 +90,12 @@ describe('LearningPathManagementComponent', () => { expect(sortByPropertyStub).toHaveBeenCalledWith(searchResult.resultsOnPage, comp.sortedColumn, comp.listSorting); }; - it('should load course on init', fakeAsync(() => { + it('should load learning paths enabled on init', fakeAsync(() => { setStateAndCallOnInit(() => { comp.listSorting = true; tick(10); - expect(findCourseStub).toHaveBeenCalledWith(1); - expect(comp.course).toEqual(course); + expect(getCourseLearningPathsEnabledStub).toHaveBeenCalledWith(courseId); + expect(comp.learningPathsEnabled).toBeTrue(); }); })); @@ -108,7 +104,7 @@ describe('LearningPathManagementComponent', () => { setStateAndCallOnInit(() => { comp.listSorting = false; tick(10); - expect(searchForLearningPathsStub).toHaveBeenCalledWith({ ...state, sortingOrder: SortingOrder.DESCENDING }, 1); + expect(searchForLearningPathsStub).toHaveBeenCalledWith({ ...state, sortingOrder: SortingOrder.DESCENDING }, courseId); expect(comp.listSorting).toBeFalse(); }); })); @@ -118,7 +114,7 @@ describe('LearningPathManagementComponent', () => { setStateAndCallOnInit(() => { comp.onPageChange(5); tick(10); - expect(searchForLearningPathsStub).toHaveBeenCalledWith({ ...state, page: 5 }, course.id); + expect(searchForLearningPathsStub).toHaveBeenCalledWith({ ...state, page: 5 }, courseId); expect(comp.page).toBe(5); }); })); @@ -131,7 +127,7 @@ describe('LearningPathManagementComponent', () => { tick(10); expect(searchForLearningPathsStub).not.toHaveBeenCalled(); tick(290); - expect(searchForLearningPathsStub).toHaveBeenCalledWith({ ...state, searchTerm: givenSearchTerm }, course.id); + expect(searchForLearningPathsStub).toHaveBeenCalledWith({ ...state, searchTerm: givenSearchTerm }, courseId); expect(comp.searchTerm).toEqual(givenSearchTerm); }); })); @@ -141,7 +137,7 @@ describe('LearningPathManagementComponent', () => { setStateAndCallOnInit(() => { comp.sortedColumn = TableColumn.USER_LOGIN; tick(10); - expect(searchForLearningPathsStub).toHaveBeenCalledWith({ ...state, sortedColumn: TableColumn.USER_LOGIN }, course.id); + expect(searchForLearningPathsStub).toHaveBeenCalledWith({ ...state, sortedColumn: TableColumn.USER_LOGIN }, courseId); expect(comp.sortedColumn).toEqual(TableColumn.USER_LOGIN); }); })); From 7cb2c3312165981915fb4064e1f9dcbf99b118d9 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Sat, 29 Jul 2023 12:21:51 +0200 Subject: [PATCH 089/215] fix sidebar comments --- .../participate/learning-path-graph-sidebar.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.ts b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.ts index 68cc20c26bcf..676656f48bca 100644 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.ts +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.ts @@ -25,12 +25,12 @@ export class LearningPathGraphSidebarComponent implements AfterViewInit { @Output() nodeClicked: EventEmitter = new EventEmitter(); ngAfterViewInit(): void { - // allows the conversation sidebar to be resized towards the right-hand side + // allows the sidebar to be resized towards the right-hand side interact('.expanded-graph') .resizable({ edges: { left: false, right: '.draggable-right', bottom: false, top: false }, modifiers: [ - // Set maximum width of the conversation sidebar + // Set maximum width of the sidebar interact.modifiers!.restrictSize({ min: { width: 230, height: 0 }, max: { width: 500, height: 4000 }, From f36f3c67aa7fe32f2ac33d125f813dfc8f02dbe5 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Sat, 29 Jul 2023 15:00:05 +0200 Subject: [PATCH 090/215] resolve merge conflict --- .../learning-path-management.component.spec.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts b/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts index da1f5ec402e6..9d4fddf49670 100644 --- a/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts @@ -106,16 +106,14 @@ describe('LearningPathManagementComponent', () => { })); it('should enable learning paths and load data', fakeAsync(() => { - const disabledCourse = Object.assign({}, course); - disabledCourse.learningPathsEnabled = false; - findCourseStub.mockReturnValueOnce(of(new HttpResponse({ body: disabledCourse }))).mockReturnValueOnce(course); + getCourseLearningPathsEnabledStub.mockReturnValueOnce(of(new HttpResponse({ body: false }))).mockReturnValueOnce(of(new HttpResponse({ body: true }))); fixture.detectChanges(); comp.ngOnInit(); comp.enableLearningPaths(); expect(enableLearningPathsStub).toHaveBeenCalledOnce(); - expect(enableLearningPathsStub).toHaveBeenCalledWith(course.id); - expect(findCourseStub).toHaveBeenCalledTimes(3); - expect(comp.course.learningPathsEnabled).toBeTruthy(); + expect(enableLearningPathsStub).toHaveBeenCalledWith(courseId); + expect(getCourseLearningPathsEnabledStub).toHaveBeenCalledTimes(3); + expect(comp.learningPathsEnabled).toBeTruthy(); })); it('should set content to paging result on sort', fakeAsync(() => { From 77fcfc7f87e62d7e53d8b955a2508595c371d6fe Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Sat, 29 Jul 2023 15:02:51 +0200 Subject: [PATCH 091/215] Fix order of course management tab link --- .../course-management-tab-bar.component.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html b/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html index a7ff47c0d360..c0aef9418d43 100644 --- a/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html +++ b/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html @@ -17,14 +17,14 @@ Competencies - - - Statistics - Learning Paths + + + Statistics + Date: Sat, 29 Jul 2023 16:43:57 +0200 Subject: [PATCH 092/215] fix minor style issues --- .../learning-path-management.component.html | 2 +- .../learning-path-management.component.ts | 2 +- ...earning-path-progress-modal.component.html | 3 ++- ...earning-path-progress-modal.component.scss | 1 + .../learning-path-progress-modal.component.ts | 3 ++- .../learning-path-progress-nav.component.html | 19 +++++++++++------- .../learning-path-progress-nav.component.ts | 4 +++- src/main/webapp/i18n/de/competency.json | 8 ++++++-- src/main/webapp/i18n/en/competency.json | 8 ++++++-- ...ning-path-progress-modal.component.spec.ts | 5 +++-- ...arning-path-progress-nav.component.spec.ts | 20 +++++++++++++++++-- 11 files changed, 55 insertions(+), 20 deletions(-) diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html index b6db8b8f0dc0..7036b9a7016c 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html @@ -64,7 +64,7 @@

    Learning Pa - + diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts index b27a8d185900..721dde1dd465 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts @@ -205,6 +205,6 @@ export class LearningPathManagementComponent implements OnInit { backdrop: 'static', windowClass: 'learning-path-modal', }); - modalRef.componentInstance.learningPathId = learningPath.id; + modalRef.componentInstance.learningPath = learningPath; } } diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.html b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.html index 4b38a926a616..4d1b65360501 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.html @@ -1,12 +1,13 @@ diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.scss b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.scss index 5db8905472ca..cc798cd46adb 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.scss +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.scss @@ -17,6 +17,7 @@ .graph { width: 100%; + overflow: hidden; } } diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.ts index c898cfde1f28..3e87482443c0 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.ts @@ -1,13 +1,14 @@ import { Component, Input, ViewChild } from '@angular/core'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { LearningPathGraphComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph.component'; +import { LearningPathPageableSearchDTO } from 'app/entities/competency/learning-path.model'; @Component({ selector: 'jhi-learning-path-progress-modal', styleUrls: ['./learning-path-progress-modal.component.scss'], templateUrl: './learning-path-progress-modal.component.html', }) export class LearningPathProgressModalComponent { - @Input() learningPathId: number; + @Input() learningPath: LearningPathPageableSearchDTO; @ViewChild('learningPathGraphComponent') learningPathGraphComponent: LearningPathGraphComponent; constructor(private activeModal: NgbActiveModal) {} diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-nav.component.html b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-nav.component.html index b5da609387c9..66f5c6778206 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-nav.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-nav.component.html @@ -1,11 +1,16 @@
    -
    - +
    +

    {{ learningPath.user?.name }} ({{ learningPath.user?.login }})

    -
    - -
    -
    - +
    + + +
    diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-nav.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-nav.component.ts index 3d968549da59..c050632cc577 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-nav.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-nav.component.ts @@ -1,11 +1,13 @@ -import { Component, EventEmitter, Output } from '@angular/core'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; import { faArrowsRotate, faArrowsToEye, faXmark } from '@fortawesome/free-solid-svg-icons'; +import { LearningPathPageableSearchDTO } from 'app/entities/competency/learning-path.model'; @Component({ selector: 'jhi-learning-path-progress-nav', templateUrl: './learning-path-progress-nav.component.html', }) export class LearningPathProgressNavComponent { + @Input() learningPath: LearningPathPageableSearchDTO; @Output() onRefresh: EventEmitter = new EventEmitter(); @Output() onCenterView: EventEmitter = new EventEmitter(); @Output() onClose: EventEmitter = new EventEmitter(); diff --git a/src/main/webapp/i18n/de/competency.json b/src/main/webapp/i18n/de/competency.json index 74db1b570c43..05ef50f5b864 100644 --- a/src/main/webapp/i18n/de/competency.json +++ b/src/main/webapp/i18n/de/competency.json @@ -160,8 +160,12 @@ "table": { "name": "Name", "login": "Login", - "progress": "Fortschritt", - "view": "Ansehen" + "progress": "Fortschritt" + }, + "progressNav": { + "header": "Lernpfad", + "refresh": "Refresh", + "center": "Center view" } }, "sideBar": { diff --git a/src/main/webapp/i18n/en/competency.json b/src/main/webapp/i18n/en/competency.json index 3b7cb1d04568..a299129239be 100644 --- a/src/main/webapp/i18n/en/competency.json +++ b/src/main/webapp/i18n/en/competency.json @@ -159,8 +159,12 @@ "table": { "name": "Name", "login": "Login", - "progress": "Progress", - "view": "View" + "progress": "Progress" + }, + "progressNav": { + "header": "Learning Path", + "refresh": "Refresh", + "center": "Center view" } }, "sideBar": { diff --git a/src/test/javascript/spec/component/learning-paths/management/learning-path-progress-modal.component.spec.ts b/src/test/javascript/spec/component/learning-paths/management/learning-path-progress-modal.component.spec.ts index dbe4e026f787..7c57307bcbb5 100644 --- a/src/test/javascript/spec/component/learning-paths/management/learning-path-progress-modal.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/management/learning-path-progress-modal.component.spec.ts @@ -6,6 +6,7 @@ import { LearningPathProgressModalComponent } from 'app/course/learning-paths/le import { LearningPathGraphComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph.component'; import { By } from '@angular/platform-browser'; import { LearningPathProgressNavComponent } from 'app/course/learning-paths/learning-path-management/learning-path-progress-nav.component'; +import { LearningPathPageableSearchDTO } from 'app/entities/competency/learning-path.model'; describe('LearningPathProgressModalComponent', () => { let fixture: ComponentFixture; @@ -33,8 +34,8 @@ describe('LearningPathProgressModalComponent', () => { jest.restoreAllMocks(); }); - it('should display learning path graph if id is present', () => { - comp.learningPathId = 1; + it('should display learning path graph if learning path is present', () => { + comp.learningPath = { id: 1 } as LearningPathPageableSearchDTO; fixture.detectChanges(); expect(fixture.debugElement.query(By.css('.graph')).nativeElement).toBeTruthy(); }); diff --git a/src/test/javascript/spec/component/learning-paths/management/learning-path-progress-nav.component.spec.ts b/src/test/javascript/spec/component/learning-paths/management/learning-path-progress-nav.component.spec.ts index 84eca7eebcb3..437b2781c3ea 100644 --- a/src/test/javascript/spec/component/learning-paths/management/learning-path-progress-nav.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/management/learning-path-progress-nav.component.spec.ts @@ -2,6 +2,11 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ArtemisTestModule } from '../../../test.module'; import { By } from '@angular/platform-browser'; import { LearningPathProgressNavComponent } from 'app/course/learning-paths/learning-path-management/learning-path-progress-nav.component'; +import { LearningPathPageableSearchDTO } from 'app/entities/competency/learning-path.model'; +import { UserNameAndLoginDTO } from 'app/core/user/user.model'; +import { MockPipe } from 'ng-mocks'; +import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { NgbTooltipMocksModule } from '../../../helpers/mocks/directive/ngbTooltipMocks.module'; describe('LearningPathProgressNavComponent', () => { let fixture: ComponentFixture; @@ -9,11 +14,12 @@ describe('LearningPathProgressNavComponent', () => { let onRefreshStub: jest.SpyInstance; let onCenterViewStub: jest.SpyInstance; let onCloseStub: jest.SpyInstance; + let learningPath: LearningPathPageableSearchDTO; beforeEach(() => { TestBed.configureTestingModule({ - imports: [ArtemisTestModule], - declarations: [LearningPathProgressNavComponent], + imports: [ArtemisTestModule, NgbTooltipMocksModule], + declarations: [LearningPathProgressNavComponent, MockPipe(ArtemisTranslatePipe)], providers: [], }) .compileComponents() @@ -23,6 +29,11 @@ describe('LearningPathProgressNavComponent', () => { onRefreshStub = jest.spyOn(comp.onRefresh, 'emit'); onCenterViewStub = jest.spyOn(comp.onCenterView, 'emit'); onCloseStub = jest.spyOn(comp.onClose, 'emit'); + learningPath = new LearningPathPageableSearchDTO(); + learningPath.user = new UserNameAndLoginDTO(); + learningPath.user.name = 'some arbitrary name'; + learningPath.user.login = 'somearbitrarylogin'; + comp.learningPath = learningPath; fixture.detectChanges(); }); }); @@ -31,6 +42,11 @@ describe('LearningPathProgressNavComponent', () => { jest.restoreAllMocks(); }); + it('should create', () => { + expect(fixture).toBeTruthy(); + expect(comp).toBeTruthy(); + }); + it('should emit refresh on click', () => { const button = fixture.debugElement.query(By.css('#refresh-button')); expect(button).not.toBeNull(); From c66f26e669caa80c62b85f8f23df765581c25e17 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Sat, 29 Jul 2023 18:44:24 +0200 Subject: [PATCH 093/215] add learning path health check endpoint --- .../repository/LearningPathRepository.java | 7 ++ .../artemis/service/LearningPathService.java | 23 +++++++ .../web/rest/LearningPathResource.java | 23 ++++++- .../dto/competency/LearningPathHealthDTO.java | 17 +++++ .../lecture/LearningPathIntegrationTest.java | 17 ++++- .../service/LearningPathServiceTest.java | 67 ++++++++++++++----- 6 files changed, 133 insertions(+), 21 deletions(-) create mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/LearningPathHealthDTO.java diff --git a/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java index 81acb13fbff2..4a9cca28bcd1 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java @@ -72,4 +72,11 @@ default LearningPath findWithEagerCompetenciesAndLearningObjectsByIdElseThrow(lo default LearningPath findWithEagerCompetenciesAndLearningObjectsAndCompletedUsersByIdElseThrow(long learningPathId) { return findWithEagerCompetenciesAndLearningObjectsAndCompletedUsersById(learningPathId).orElseThrow(() -> new EntityNotFoundException("LearningPath", learningPathId)); } + + @Query(""" + SELECT COUNT (learningPath) + FROM LearningPath learningPath + WHERE learningPath.course.id = :#{#courseId} + """) + Long countByCourseId(@Param("courseId") long courseId); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index 9a2fc5ca4922..4009c7314fae 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -23,6 +23,7 @@ import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.web.rest.dto.PageableSearchDTO; import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; +import de.tum.in.www1.artemis.web.rest.dto.competency.LearningPathHealthDTO; import de.tum.in.www1.artemis.web.rest.dto.competency.LearningPathPageableSearchDTO; import de.tum.in.www1.artemis.web.rest.dto.competency.NgxLearningPathDTO; import de.tum.in.www1.artemis.web.rest.util.PageUtil; @@ -410,4 +411,26 @@ public LearningObject getRecommendation(@NotNull LearningPath learningPath) { .filter(exercise -> !exerciseRepository.findByIdWithStudentParticipationsElseThrow(exercise.getId()).isCompletedFor(learningPath.getUser())))) .findFirst().orElse(null); } + + /** + * Gets the health status of learning paths for the given course. + * + * @param course the course for which the health status should be generated + * @return dto containing the health status and additional information (missing learning paths) if needed + */ + public LearningPathHealthDTO getHealthStatusForCourse(Course course) { + if (!course.getLearningPathsEnabled()) { + return new LearningPathHealthDTO(LearningPathHealthDTO.HealthStatus.DISABLED); + } + + var numberOfStudents = userRepository.countUserInGroup(course.getStudentGroupName()); + var numberOfLearningPaths = learningPathRepository.countByCourseId(course.getId()); + + if (numberOfStudents.equals(numberOfLearningPaths)) { + return new LearningPathHealthDTO(LearningPathHealthDTO.HealthStatus.OK); + } + else { + return new LearningPathHealthDTO(LearningPathHealthDTO.HealthStatus.MISSING, numberOfStudents - numberOfLearningPaths); + } + } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java index 37db0dff1934..424794338f08 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java @@ -22,9 +22,7 @@ import de.tum.in.www1.artemis.service.LearningPathService; import de.tum.in.www1.artemis.web.rest.dto.PageableSearchDTO; import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; -import de.tum.in.www1.artemis.web.rest.dto.competency.LearningPathPageableSearchDTO; -import de.tum.in.www1.artemis.web.rest.dto.competency.LearningPathRecommendationDTO; -import de.tum.in.www1.artemis.web.rest.dto.competency.NgxLearningPathDTO; +import de.tum.in.www1.artemis.web.rest.dto.competency.*; import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException; @RestController @@ -170,4 +168,23 @@ else if (recommendation instanceof LectureUnit lectureUnit) { return ResponseEntity.ok(new LearningPathRecommendationDTO(recommendation.getId(), -1, LearningPathRecommendationDTO.RecommendationType.EXERCISE)); } } + + /** + * GET /courses/:courseId/learning-path-health : Gets the health status of learning paths for the course. + * + * @param courseId the id of the course for which the health status should be fetched + * @return the ResponseEntity with status 200 (OK) and with body the health status + */ + @GetMapping("/courses/{courseId}/learning-path-health") + @EnforceAtLeastInstructor + public ResponseEntity getHealthStatusForCourse(@PathVariable long courseId) { + log.debug("REST request to get health status of learning paths in course with id: {}", courseId); + + Course course = courseRepository.findByIdElseThrow(courseId); + if (!authorizationCheckService.isAtLeastInstructorInCourse(course, null) && !authorizationCheckService.isAdmin()) { + throw new AccessForbiddenException("You are not allowed to access the health status of learning paths for this course."); + } + + return ResponseEntity.ok(learningPathService.getHealthStatusForCourse(course)); + } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/LearningPathHealthDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/LearningPathHealthDTO.java new file mode 100644 index 000000000000..f261bf7cb26a --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/LearningPathHealthDTO.java @@ -0,0 +1,17 @@ +package de.tum.in.www1.artemis.web.rest.dto.competency; + +import javax.validation.constraints.NotNull; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record LearningPathHealthDTO(@NotNull HealthStatus status, Long missingLearningPaths) { + + public LearningPathHealthDTO(HealthStatus status) { + this(status, null); + } + + public enum HealthStatus { + OK, DISABLED, MISSING + } +} diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java index 011ce9c9bdc7..ec17beeb9ef7 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java @@ -36,9 +36,7 @@ import de.tum.in.www1.artemis.service.LectureUnitService; import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.util.PageableSearchUtilService; -import de.tum.in.www1.artemis.web.rest.dto.competency.LearningPathPageableSearchDTO; -import de.tum.in.www1.artemis.web.rest.dto.competency.LearningPathRecommendationDTO; -import de.tum.in.www1.artemis.web.rest.dto.competency.NgxLearningPathDTO; +import de.tum.in.www1.artemis.web.rest.dto.competency.*; class LearningPathIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { @@ -156,6 +154,7 @@ private void testAllPreAuthorize() throws Exception { request.putWithResponseBody("/api/courses/" + course.getId() + "/learning-paths/enable", null, Course.class, HttpStatus.FORBIDDEN); final var search = pageableSearchUtilService.configureSearch(""); request.getSearchResult("/api/courses/" + course.getId() + "/learning-paths", HttpStatus.FORBIDDEN, LearningPath.class, pageableSearchUtilService.searchMapping(search)); + request.get("/api/courses/" + course.getId() + "/learning-path-health", HttpStatus.FORBIDDEN, LearningPathHealthDTO.class); } private void enableLearningPathsRESTCall(Course course) throws Exception { @@ -428,4 +427,16 @@ void testGetRecommendationAsOwner() throws Exception { final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); request.get("/api/learning-path/" + learningPath.getId() + "/recommendation", HttpStatus.OK, LearningPathRecommendationDTO.class); } + + /** + * This only tests if the end point successfully retrieves the health status. The correctness of the health status is tested in LearningPathServiceTest. + * + * @throws Exception the request failed + * @see de.tum.in.www1.artemis.service.LearningPathServiceTest + */ + @Test + @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") + void testGetHealthStatusForCourse() throws Exception { + request.get("/api/courses/" + course.getId() + "/learning-path-health", HttpStatus.OK, LearningPathHealthDTO.class); + } } diff --git a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java index 1588ae37b389..c878177504de 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.time.ZonedDateTime; import java.util.Arrays; import java.util.HashSet; import java.util.Set; @@ -15,6 +16,7 @@ import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; import de.tum.in.www1.artemis.competency.CompetencyUtilService; import de.tum.in.www1.artemis.competency.LearningPathUtilService; +import de.tum.in.www1.artemis.course.CourseFactory; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.Exercise; @@ -26,12 +28,17 @@ import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.lecture.LectureUtilService; import de.tum.in.www1.artemis.repository.CompetencyRepository; +import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.LearningPathRepository; import de.tum.in.www1.artemis.security.SecurityUtils; +import de.tum.in.www1.artemis.user.UserUtilService; +import de.tum.in.www1.artemis.web.rest.dto.competency.LearningPathHealthDTO; import de.tum.in.www1.artemis.web.rest.dto.competency.NgxLearningPathDTO; class LearningPathServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { + private static final String TEST_PREFIX = "learningpathservice"; + @Autowired LearningPathService learningPathService; @@ -56,6 +63,12 @@ class LearningPathServiceTest extends AbstractSpringIntegrationBambooBitbucketJi @Autowired CompetencyRepository competencyRepository; + @Autowired + UserUtilService userUtilService; + + @Autowired + CourseRepository courseRepository; + private Course course; private void generateAndAssert(NgxLearningPathDTO expected) { @@ -72,6 +85,11 @@ private void assertNgxRepEquals(NgxLearningPathDTO was, NgxLearningPathDTO expec assertThat(was.clusters()).as("correct clusters").containsExactlyInAnyOrderElementsOf(expected.clusters()); } + @BeforeEach + void setAuthorizationForRepositoryRequests() { + SecurityUtils.setAuthorizationObject(); + } + @BeforeEach void setup() { course = courseUtilService.createCourse(); @@ -80,11 +98,6 @@ void setup() { @Nested class GenerateNgxRepresentationBaseTest { - @BeforeEach - void setAuthorizationForRepositoryRequests() { - SecurityUtils.setAuthorizationObject(); - } - @Test void testEmptyLearningPath() { NgxLearningPathDTO expected = new NgxLearningPathDTO(Set.of(), Set.of(), Set.of()); @@ -163,11 +176,6 @@ class GenerateNgxRepresentationRelationTest { Set expectedClusters; - @BeforeEach - void setAuthorizationForRepositoryRequests() { - SecurityUtils.setAuthorizationObject(); - } - @BeforeEach void setup() { competency1 = competencyUtilService.createCompetency(course); @@ -274,11 +282,6 @@ private static NgxLearningPathDTO.Node getNodeForExercise(Competency competency, @Nested class RecommendationTest { - @BeforeEach - void setAuthorizationForRepositoryRequests() { - SecurityUtils.setAuthorizationObject(); - } - @Test void testGetRecommendationEmpty() { competencyUtilService.createCompetency(course); @@ -297,4 +300,38 @@ void testGetRecommendationNotEmpty() { assertThat(learningPathService.getRecommendation(learningPath)).isNotNull(); } } + + @Nested + class HeathCheckTest { + + @BeforeEach + void setup() { + userUtilService.addUsers(TEST_PREFIX, 5, 1, 1, 1); + 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 + void testHealthStatusDisabled() { + var healthStatus = learningPathService.getHealthStatusForCourse(course); + assertThat(healthStatus.status()).isEqualTo(LearningPathHealthDTO.HealthStatus.DISABLED); + } + + @Test + void testHealthStatusOK() { + course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); + var healthStatus = learningPathService.getHealthStatusForCourse(course); + assertThat(healthStatus.status()).isEqualTo(LearningPathHealthDTO.HealthStatus.OK); + } + + @Test + void testHealthStatusMissing() { + course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); + userUtilService.addStudent(TEST_PREFIX + "tumuser", TEST_PREFIX + "student1337"); + var healthStatus = learningPathService.getHealthStatusForCourse(course); + assertThat(healthStatus.status()).isEqualTo(LearningPathHealthDTO.HealthStatus.MISSING); + assertThat(healthStatus.missingLearningPaths()).isEqualTo(1); + } + } } From 6a1ad9e9158e2cf709c865329c6457996b07d53a Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Sun, 30 Jul 2023 03:21:09 +0200 Subject: [PATCH 094/215] add learning path health check client side --- .../web/rest/LearningPathResource.java | 23 ++++++++- .../learning-path-management.component.html | 15 +++++- .../learning-path-management.component.ts | 47 ++++++++++--------- .../learning-paths/learning-path.service.ts | 9 ++++ .../competency/learning-path-health.model.ts | 14 ++++++ src/main/webapp/i18n/de/competency.json | 8 ++++ src/main/webapp/i18n/en/competency.json | 8 ++++ .../competency/LearningPathUtilService.java | 26 ++++++++-- .../lecture/LearningPathIntegrationTest.java | 26 +++++++++- ...learning-path-management.component.spec.ts | 45 ++++++++++++------ .../service/learning-path.service.spec.ts | 12 +++++ 11 files changed, 189 insertions(+), 44 deletions(-) create mode 100644 src/main/webapp/app/entities/competency/learning-path-health.model.ts diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java index 424794338f08..5c1bae9fd858 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java @@ -54,7 +54,7 @@ public LearningPathResource(CourseRepository courseRepository, AuthorizationChec * PUT /courses/:courseId/learning-paths/enable : Enables and generates learning paths for the course * * @param courseId the id of the course for which the learning paths should be enabled - * @return the ResponseEntity with status 200 (OK) and with body the updated course + * @return the ResponseEntity with status 200 (OK) */ @PutMapping("/courses/{courseId}/learning-paths/enable") @EnforceAtLeastInstructor @@ -68,11 +68,30 @@ public ResponseEntity enableLearningPathsForCourse(@PathVariable Long cour course.setLearningPathsEnabled(true); learningPathService.generateLearningPaths(course); - course = courseRepository.save(course); + courseRepository.save(course); return ResponseEntity.ok().build(); } + /** + * PUT /courses/:courseId/learning-paths/generate-missing : Generates missing learning paths for the course + * + * @param courseId the id of the course for which the learning paths should be created + * @return the ResponseEntity with status 200 (OK) + */ + @PutMapping("/courses/{courseId}/learning-paths/generate-missing") + @EnforceAtLeastInstructor + public ResponseEntity generateMissingLearningPathsForCourse(@PathVariable Long courseId) { + log.debug("REST request to generate missing learning paths for course with id: {}", courseId); + Course course = courseRepository.findWithEagerCompetenciesByIdElseThrow(courseId); + authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null); + if (!course.getLearningPathsEnabled()) { + throw new BadRequestException("Learning paths not enabled for this course."); + } + learningPathService.generateLearningPaths(course); + return ResponseEntity.ok().build(); + } + /** * GET /courses/:courseId/learning-paths : Gets all the learning paths of a course. The result is pageable. * diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html index 7036b9a7016c..4d85780caa13 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html @@ -6,7 +6,7 @@

    Learning Path Management

    -
    +
    Disabled
    -
    +
    +
    +
    {{ 'artemisApp.learningPath.manageLearningPaths.health.missing.title' | artemisTranslate }}
    +

    {{ 'artemisApp.learningPath.manageLearningPaths.health.missing.body' | artemisTranslate }}

    + +
    +
    +
    Search for Learning Path: diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts index 721dde1dd465..11ce6158e1f7 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts @@ -1,6 +1,5 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { CourseManagementService } from 'app/course/manage/course-management.service'; import { Subject } from 'rxjs'; import { LearningPathService } from 'app/course/learning-paths/learning-path.service'; import { debounceTime, finalize, switchMap, tap } from 'rxjs/operators'; @@ -11,9 +10,10 @@ import { PageableSearch, SearchResult, SortingOrder } from 'app/shared/table/pag import { LearningPathPagingService } from 'app/course/learning-paths/learning-path-paging.service'; import { SortService } from 'app/shared/service/sort.service'; import { LearningPathPageableSearchDTO } from 'app/entities/competency/learning-path.model'; -import { faSort } from '@fortawesome/free-solid-svg-icons'; +import { faSort, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { LearningPathProgressModalComponent } from 'app/course/learning-paths/learning-path-management/learning-path-progress-modal.component'; +import { HealthStatus, LearningPathHealthDTO } from 'app/entities/competency/learning-path-health.model'; export enum TableColumn { ID = 'ID', @@ -30,7 +30,7 @@ export class LearningPathManagementComponent implements OnInit { isLoading = false; courseId: number; - learningPathsEnabled: boolean; + health: LearningPathHealthDTO; searchLoading = false; readonly column = TableColumn; @@ -49,10 +49,10 @@ export class LearningPathManagementComponent implements OnInit { // icons faSort = faSort; + faTriangleExclamation = faTriangleExclamation; constructor( private activatedRoute: ActivatedRoute, - private courseManagementService: CourseManagementService, private learningPathService: LearningPathService, private alertService: AlertService, private pagingService: LearningPathPagingService, @@ -124,8 +124,8 @@ export class LearningPathManagementComponent implements OnInit { private loadData() { this.isLoading = true; - this.courseManagementService - .getCourseLearningPathsEnabled(this.courseId) + this.learningPathService + .getHealthStatusForCourse(this.courseId) .pipe( finalize(() => { this.isLoading = false; @@ -133,8 +133,8 @@ export class LearningPathManagementComponent implements OnInit { ) .subscribe({ next: (res) => { - this.learningPathsEnabled = res.body!; - if (this.learningPathsEnabled) { + this.health = res.body!; + if (this.health.status !== HealthStatus.DISABLED) { this.performSearch(this.sort, 0); this.performSearch(this.search, 300); } @@ -145,19 +145,22 @@ export class LearningPathManagementComponent implements OnInit { enableLearningPaths() { this.isLoading = true; - this.learningPathService - .enableLearningPaths(this.courseId) - .pipe( - finalize(() => { - this.loadData(); - }), - ) - .subscribe({ - next: () => { - this.learningPathsEnabled = true; - }, - error: (res: HttpErrorResponse) => onError(this.alertService, res), - }); + this.learningPathService.enableLearningPaths(this.courseId).subscribe({ + next: () => { + this.loadData(); + }, + error: (res: HttpErrorResponse) => onError(this.alertService, res), + }); + } + + generateMissing() { + this.isLoading = true; + this.learningPathService.generateMissingLearningPathsForCourse(this.courseId).subscribe({ + next: () => { + this.loadData(); + }, + error: (res: HttpErrorResponse) => onError(this.alertService, res), + }); } /** @@ -207,4 +210,6 @@ export class LearningPathManagementComponent implements OnInit { }); modalRef.componentInstance.learningPath = learningPath; } + + protected readonly HealthStatus = HealthStatus; } diff --git a/src/main/webapp/app/course/learning-paths/learning-path.service.ts b/src/main/webapp/app/course/learning-paths/learning-path.service.ts index 5f2f66d54d8d..7fc0e33a1a3c 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path.service.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path.service.ts @@ -3,6 +3,7 @@ import { Observable } from 'rxjs'; import { HttpClient, HttpResponse } from '@angular/common/http'; import { LearningPathRecommendationDTO, NgxLearningPathDTO } from 'app/entities/competency/learning-path.model'; import { map } from 'rxjs/operators'; +import { LearningPathHealthDTO } from 'app/entities/competency/learning-path-health.model'; @Injectable({ providedIn: 'root' }) export class LearningPathService { @@ -14,6 +15,10 @@ export class LearningPathService { return this.httpClient.put(`${this.resourceURL}/courses/${courseId}/learning-paths/enable`, null, { observe: 'response' }); } + generateMissingLearningPathsForCourse(courseId: number): Observable> { + return this.httpClient.put(`${this.resourceURL}/courses/${courseId}/learning-paths/generate-missing`, null, { observe: 'response' }); + } + getLearningPathId(courseId: number) { return this.httpClient.get(`${this.resourceURL}/courses/${courseId}/learning-path-id`, { observe: 'response' }); } @@ -38,4 +43,8 @@ export class LearningPathService { getRecommendation(learningPathId: number) { return this.httpClient.get(`${this.resourceURL}/learning-path/${learningPathId}/recommendation`, { observe: 'response' }); } + + getHealthStatusForCourse(courseId: number) { + return this.httpClient.get(`${this.resourceURL}/courses/${courseId}/learning-path-health`, { observe: 'response' }); + } } diff --git a/src/main/webapp/app/entities/competency/learning-path-health.model.ts b/src/main/webapp/app/entities/competency/learning-path-health.model.ts new file mode 100644 index 000000000000..bf3a9794d178 --- /dev/null +++ b/src/main/webapp/app/entities/competency/learning-path-health.model.ts @@ -0,0 +1,14 @@ +export class LearningPathHealthDTO { + public status?: HealthStatus; + public missingLearningPaths?: number; + + constructor(status: HealthStatus) { + this.status = status; + } +} + +export enum HealthStatus { + OK = 'OK', + DISABLED = 'DISABLED', + MISSING = 'MISSING', +} diff --git a/src/main/webapp/i18n/de/competency.json b/src/main/webapp/i18n/de/competency.json index 05ef50f5b864..c2050cb16f39 100644 --- a/src/main/webapp/i18n/de/competency.json +++ b/src/main/webapp/i18n/de/competency.json @@ -153,6 +153,14 @@ "learningPathButton": "Lernpfade", "manageLearningPaths": { "title": "Lernpfadmanagement", + "health": { + "missing": { + "title": "Fehlende Lernpfade", + "body": "Für einige Studierende wurde noch kein Lernpfad erstellt. Dies ist nicht kritisch. Ihre Lernpfade werden generiert, wenn sie ihren Lernpfad das erste mal anfragen.", + "action": "Erstellen", + "hint": "Erstellen der fehlenden Lernpfade" + } + }, "isDisabled": "Lernpfade sind für diesen Kurs nicht aktiviert.", "enable": "Lernpfade aktivieren", "enableHint": "Die Erstellung von Lernpfaden für alle Studierende kann einige Minuten dauern.", diff --git a/src/main/webapp/i18n/en/competency.json b/src/main/webapp/i18n/en/competency.json index a299129239be..195dd6b9f885 100644 --- a/src/main/webapp/i18n/en/competency.json +++ b/src/main/webapp/i18n/en/competency.json @@ -152,6 +152,14 @@ "learningPathButton": "Learning Paths", "manageLearningPaths": { "title": "Learning Path Management", + "health": { + "missing": { + "title": "Missing Learning Paths", + "body": "Some student's have not generated their Learning Paths yet. This is not critical. Their Learning Paths will be created once they request their learning path for the first time.", + "action": "Generate", + "hint": "Generate missing Learning Paths" + } + }, "isDisabled": "Learning Paths are currently disabled for this course.", "enable": "Enable Learning Paths", "enableHint": "The creation of Learning Paths for every student may take a few minutes.", diff --git a/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java b/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java index 9c116978e58e..e6f580eddb99 100644 --- a/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java @@ -6,11 +6,10 @@ import org.springframework.stereotype.Service; import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.competency.LearningPath; -import de.tum.in.www1.artemis.repository.CompetencyRepository; -import de.tum.in.www1.artemis.repository.CourseRepository; -import de.tum.in.www1.artemis.repository.LearningPathRepository; +import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.service.LearningPathService; /** @@ -31,6 +30,9 @@ public class LearningPathUtilService { @Autowired CompetencyRepository competencyRepository; + @Autowired + UserRepository userRepository; + /** * Enable and generate learning paths for course. * @@ -68,4 +70,22 @@ public LearningPath createLearningPath(Set competencies) { learningPath.setCompetencies(competencies); return learningPathRepository.save(learningPath); } + + /** + * Deletes all learning paths of given user. + * + * @param user the user for which all learning paths should be deleted + */ + public void deleteLearningPaths(User user) { + learningPathRepository.deleteAll(user.getLearningPaths()); + } + + /** + * Deletes all learning paths of all given user. + * + * @param users the users for which all learning paths should be deleted + */ + public void deleteLearningPaths(Iterable users) { + users.forEach(this::deleteLearningPaths); + } } diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java index ec17beeb9ef7..6d7d1365ace8 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java @@ -151,7 +151,8 @@ private ZonedDateTime future(long days) { } private void testAllPreAuthorize() throws Exception { - request.putWithResponseBody("/api/courses/" + course.getId() + "/learning-paths/enable", null, Course.class, HttpStatus.FORBIDDEN); + request.put("/api/courses/" + course.getId() + "/learning-paths/enable", null, HttpStatus.FORBIDDEN); + request.put("/api/courses/" + course.getId() + "/learning-paths/generate-missing", null, HttpStatus.FORBIDDEN); final var search = pageableSearchUtilService.configureSearch(""); request.getSearchResult("/api/courses/" + course.getId() + "/learning-paths", HttpStatus.FORBIDDEN, LearningPath.class, pageableSearchUtilService.searchMapping(search)); request.get("/api/courses/" + course.getId() + "/learning-path-health", HttpStatus.FORBIDDEN, LearningPathHealthDTO.class); @@ -226,7 +227,28 @@ void testEnableLearningPathsWithNoCompetencies() throws Exception { void testEnableLearningPathsAlreadyEnabled() throws Exception { course.setLearningPathsEnabled(true); courseRepository.save(course); - request.putWithResponseBody("/api/courses/" + course.getId() + "/learning-paths/enable", course, Course.class, HttpStatus.BAD_REQUEST); + request.put("/api/courses/" + course.getId() + "/learning-paths/enable", null, HttpStatus.BAD_REQUEST); + } + + @Test + @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") + void testGenerateMissingLearningPathsForCourse() throws Exception { + course.setLearningPathsEnabled(true); + courseRepository.save(course); + final var students = userRepository.getStudents(course); + students.stream().map(User::getId).map(userRepository::findWithLearningPathsByIdElseThrow).forEach(learningPathUtilService::deleteLearningPaths); + request.put("/api/courses/" + course.getId() + "/learning-paths/generate-missing", null, HttpStatus.OK); + students.forEach(user -> { + user = userRepository.findWithLearningPathsByIdElseThrow(user.getId()); + assertThat(user.getLearningPaths().size()).isEqualTo(1); + }); + + } + + @Test + @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") + void testGenerateMissingLearningPathsForCourseNotEnabled() throws Exception { + request.put("/api/courses/" + course.getId() + "/learning-paths/generate-missing", null, HttpStatus.BAD_REQUEST); } @Test diff --git a/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts b/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts index 9d4fddf49670..e2ab1499590a 100644 --- a/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts @@ -5,22 +5,20 @@ import { SortService } from 'app/shared/service/sort.service'; import { PageableSearch, SearchResult, SortingOrder } from 'app/shared/table/pageable-table'; import { LearningPath } from 'app/entities/competency/learning-path.model'; import { ArtemisTestModule } from '../../../test.module'; -import { MockComponent, MockDirective, MockProvider } from 'ng-mocks'; +import { MockComponent, MockDirective } from 'ng-mocks'; import { ButtonComponent } from 'app/shared/components/button.component'; import { NgbPagination } from '@ng-bootstrap/ng-bootstrap'; import { SortByDirective } from 'app/shared/sort/sort-by.directive'; import { SortDirective } from 'app/shared/sort/sort.directive'; import { of } from 'rxjs'; -import { CourseManagementService } from 'app/course/manage/course-management.service'; import { ActivatedRoute } from '@angular/router'; import { HttpResponse } from '@angular/common/http'; import { LearningPathService } from 'app/course/learning-paths/learning-path.service'; +import { HealthStatus, LearningPathHealthDTO } from 'app/entities/competency/learning-path-health.model'; describe('LearningPathManagementComponent', () => { let fixture: ComponentFixture; let comp: LearningPathManagementComponent; - let courseManagementService: CourseManagementService; - let getCourseLearningPathsEnabledStub: jest.SpyInstance; let pagingService: LearningPathPagingService; let sortService: SortService; let searchForLearningPathsStub: jest.SpyInstance; @@ -30,6 +28,9 @@ describe('LearningPathManagementComponent', () => { let learningPath: LearningPath; let learningPathService: LearningPathService; let enableLearningPathsStub: jest.SpyInstance; + let generateMissingLearningPathsForCourseStub: jest.SpyInstance; + let getHealthStatusForCourseStub: jest.SpyInstance; + let health: LearningPathHealthDTO; let courseId: number; beforeEach(() => { TestBed.configureTestingModule({ @@ -46,21 +47,20 @@ describe('LearningPathManagementComponent', () => { }, }, }, - MockProvider(CourseManagementService), ], }) .compileComponents() .then(() => { fixture = TestBed.createComponent(LearningPathManagementComponent); comp = fixture.componentInstance; - courseManagementService = TestBed.inject(CourseManagementService); - getCourseLearningPathsEnabledStub = jest.spyOn(courseManagementService, 'getCourseLearningPathsEnabled'); pagingService = TestBed.inject(LearningPathPagingService); sortService = TestBed.inject(SortService); searchForLearningPathsStub = jest.spyOn(pagingService, 'searchForLearningPaths'); sortByPropertyStub = jest.spyOn(sortService, 'sortByProperty'); learningPathService = TestBed.inject(LearningPathService); enableLearningPathsStub = jest.spyOn(learningPathService, 'enableLearningPaths'); + generateMissingLearningPathsForCourseStub = jest.spyOn(learningPathService, 'generateMissingLearningPathsForCourse'); + getHealthStatusForCourseStub = jest.spyOn(learningPathService, 'getHealthStatusForCourse'); }); }); @@ -73,7 +73,6 @@ describe('LearningPathManagementComponent', () => { learningPath = new LearningPath(); learningPath.id = 2; courseId = 1; - getCourseLearningPathsEnabledStub.mockReturnValue(of(new HttpResponse({ body: true }))); searchResult = { numberOfPages: 3, resultsOnPage: [learningPath] }; state = { page: 1, @@ -85,6 +84,9 @@ describe('LearningPathManagementComponent', () => { }; searchForLearningPathsStub.mockReturnValue(of(searchResult)); enableLearningPathsStub.mockReturnValue(of(new HttpResponse())); + generateMissingLearningPathsForCourseStub.mockReturnValue(of(new HttpResponse())); + health = new LearningPathHealthDTO(HealthStatus.OK); + getHealthStatusForCourseStub.mockReturnValue(of(new HttpResponse({ body: health }))); }); const setStateAndCallOnInit = (middleExpectation: () => void) => { @@ -96,24 +98,39 @@ describe('LearningPathManagementComponent', () => { expect(sortByPropertyStub).toHaveBeenCalledWith(searchResult.resultsOnPage, comp.sortedColumn, comp.listSorting); }; - it('should load learning paths enabled on init', fakeAsync(() => { + it('should load health status on init', fakeAsync(() => { setStateAndCallOnInit(() => { comp.listSorting = true; tick(10); - expect(getCourseLearningPathsEnabledStub).toHaveBeenCalledWith(courseId); - expect(comp.learningPathsEnabled).toBeTrue(); + expect(getHealthStatusForCourseStub).toHaveBeenCalledWith(courseId); + expect(comp.health).toEqual(health); }); })); it('should enable learning paths and load data', fakeAsync(() => { - getCourseLearningPathsEnabledStub.mockReturnValueOnce(of(new HttpResponse({ body: false }))).mockReturnValueOnce(of(new HttpResponse({ body: true }))); + const healthDisabled = new LearningPathHealthDTO(HealthStatus.DISABLED); + getHealthStatusForCourseStub.mockReturnValueOnce(of(new HttpResponse({ body: healthDisabled }))).mockReturnValueOnce(of(new HttpResponse({ body: health }))); fixture.detectChanges(); comp.ngOnInit(); + expect(comp.health).toEqual(healthDisabled); comp.enableLearningPaths(); expect(enableLearningPathsStub).toHaveBeenCalledOnce(); expect(enableLearningPathsStub).toHaveBeenCalledWith(courseId); - expect(getCourseLearningPathsEnabledStub).toHaveBeenCalledTimes(3); - expect(comp.learningPathsEnabled).toBeTruthy(); + expect(getHealthStatusForCourseStub).toHaveBeenCalledTimes(3); + expect(comp.health).toEqual(health); + })); + + it('should generate missing learning paths and load data', fakeAsync(() => { + const healthMissing = new LearningPathHealthDTO(HealthStatus.MISSING); + getHealthStatusForCourseStub.mockReturnValueOnce(of(new HttpResponse({ body: healthMissing }))).mockReturnValueOnce(of(new HttpResponse({ body: health }))); + fixture.detectChanges(); + comp.ngOnInit(); + expect(comp.health).toEqual(healthMissing); + comp.generateMissing(); + expect(generateMissingLearningPathsForCourseStub).toHaveBeenCalledOnce(); + expect(generateMissingLearningPathsForCourseStub).toHaveBeenCalledWith(courseId); + expect(getHealthStatusForCourseStub).toHaveBeenCalledTimes(3); + expect(comp.health).toEqual(health); })); it('should set content to paging result on sort', fakeAsync(() => { diff --git a/src/test/javascript/spec/service/learning-path.service.spec.ts b/src/test/javascript/spec/service/learning-path.service.spec.ts index 6748f72a1fad..ca10b4b2ade4 100644 --- a/src/test/javascript/spec/service/learning-path.service.spec.ts +++ b/src/test/javascript/spec/service/learning-path.service.spec.ts @@ -34,6 +34,12 @@ describe('LearningPathService', () => { expect(putStub).toHaveBeenCalledWith('api/courses/1/learning-paths/enable', null, { observe: 'response' }); }); + it('should send request to the server to generate missing learning paths for course', () => { + learningPathService.generateMissingLearningPathsForCourse(1).subscribe(); + expect(putStub).toHaveBeenCalledOnce(); + expect(putStub).toHaveBeenCalledWith('api/courses/1/learning-paths/generate-missing', null, { observe: 'response' }); + }); + it('should send a request to the server to get learning path id of the current user in the course', () => { learningPathService.getLearningPathId(1).subscribe(); expect(getStub).toHaveBeenCalledOnce(); @@ -51,4 +57,10 @@ describe('LearningPathService', () => { expect(getStub).toHaveBeenCalledOnce(); expect(getStub).toHaveBeenCalledWith('api/learning-path/1/recommendation', { observe: 'response' }); }); + + it('should send a request to the server to get health status of learning paths for course', () => { + learningPathService.getHealthStatusForCourse(1).subscribe(); + expect(getStub).toHaveBeenCalledOnce(); + expect(getStub).toHaveBeenCalledWith('api/courses/1/learning-path-health', { observe: 'response' }); + }); }); From ac46483ad9a2f5abbe7aa4deb46951e1d03bdfb5 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Sun, 30 Jul 2023 14:02:44 +0200 Subject: [PATCH 095/215] add LP generation on first request --- .../artemis/service/LearningPathService.java | 5 +++-- .../web/rest/LearningPathResource.java | 13 ++++++++++- .../lecture/LearningPathIntegrationTest.java | 22 +++++++++++++++++++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index 9a2fc5ca4922..d9650a7fb2f3 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -75,11 +75,11 @@ public void generateLearningPaths(@NotNull Course course) { * @param course course that defines the learning path * @param user student for which the learning path is generated */ - public void generateLearningPathForUser(@NotNull Course course, @NotNull User user) { + public LearningPath generateLearningPathForUser(@NotNull Course course, @NotNull User user) { var existingLearningPath = learningPathRepository.findByCourseIdAndUserId(course.getId(), user.getId()); // the learning path has not to be generated if it already exits if (existingLearningPath.isPresent()) { - return; + return existingLearningPath.get(); } LearningPath lpToCreate = new LearningPath(); lpToCreate.setUser(user); @@ -88,6 +88,7 @@ public void generateLearningPathForUser(@NotNull Course course, @NotNull User us var persistedLearningPath = learningPathRepository.save(lpToCreate); log.debug("Created LearningPath (id={}) for user (id={}) in course (id={})", persistedLearningPath.getId(), user.getId(), course.getId()); updateLearningPathProgress(persistedLearningPath); + return persistedLearningPath; } /** diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java index 37db0dff1934..526b3c9d850b 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java @@ -97,6 +97,7 @@ public ResponseEntity> getLea /** * GET /courses/:courseId/learning-path-id : Gets the id of the learning path. + * If the learning path has not been generated although the course has learning paths enabled, the corresponding learning path will be created. * * @param courseId the id of the course from which the learning path id should be fetched * @return the ResponseEntity with status 200 (OK) and with body the id of the learning path @@ -110,8 +111,18 @@ public ResponseEntity getLearningPathId(@PathVariable Long courseId) { if (!course.getLearningPathsEnabled()) { throw new BadRequestException("Learning paths are not enabled for this course."); } + + // generate learning path if missing User user = userRepository.getUser(); - LearningPath learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), user.getId()); + final var learningPathOptional = learningPathRepository.findByCourseIdAndUserId(course.getId(), user.getId()); + LearningPath learningPath; + if (learningPathOptional.isEmpty()) { + course = courseRepository.findWithEagerCompetenciesByIdElseThrow(courseId); + learningPath = learningPathService.generateLearningPathForUser(course, user); + } + else { + learningPath = learningPathOptional.get(); + } return ResponseEntity.ok(learningPath.getId()); } diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java index 011ce9c9bdc7..e9f9eef7ae3f 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java @@ -283,6 +283,28 @@ void testGetLearningPathsOnPageForCourseExactlyStudent() throws Exception { assertThat(result.getResultsOnPage()).hasSize(1); } + @Test + @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") + void getLearningPathId() throws Exception { + course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); + final var student = userRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); + final var result = request.get("/api/courses/" + course.getId() + "/learning-path-id", HttpStatus.OK, Long.class); + assertThat(result).isEqualTo(learningPath.getId()); + } + + @Test + @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") + void getLearningPathIdNotExisting() throws Exception { + course.setLearningPathsEnabled(true); + course = courseRepository.save(course); + var student = userRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + student = userRepository.findWithLearningPathsByIdElseThrow(student.getId()); + learningPathRepository.deleteAll(student.getLearningPaths()); + final var result = request.get("/api/courses/" + course.getId() + "/learning-path-id", HttpStatus.OK, Long.class); + assertThat(result).isNotNull(); + } + private static Stream addCompetencyToLearningPathsOnCreateAndImportCompetencyTestProvider() { final Function createCall = (reference) -> { try { From 1cd8687116e8e05e1483aca97daa80318b2aadd0 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Sun, 30 Jul 2023 21:43:20 +0200 Subject: [PATCH 096/215] integrate Tobias' suggestions --- .../de/tum/in/www1/artemis/domain/Course.java | 1 - .../domain/competency/LearningPath.java | 1 + .../CompetencyProgressRepository.java | 2 +- .../service/CompetencyProgressService.java | 1 + .../artemis/service/LearningPathService.java | 16 ++++++++++-- .../web/rest/LearningPathResource.java | 13 +++++----- .../competency-management.component.ts | 2 +- .../learning-path-management.component.ts | 2 +- .../learning-path-container.component.ts | 2 +- src/main/webapp/i18n/de/competency.json | 4 +-- src/main/webapp/i18n/en/competency.json | 2 +- .../competency/LearningPathUtilService.java | 8 +++--- .../artemis/course/CourseTestService.java | 4 --- .../lecture/LearningPathIntegrationTest.java | 26 +++++++++---------- .../service/LearningPathServiceTest.java | 22 +++++++--------- 15 files changed, 55 insertions(+), 51 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Course.java b/src/main/java/de/tum/in/www1/artemis/domain/Course.java index be0f1b9cbfc7..b70bd517e4b1 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Course.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Course.java @@ -211,7 +211,6 @@ public class Course extends DomainObject { @OneToMany(mappedBy = "course", cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.LAZY) @JsonIgnoreProperties("course") - @OrderBy("id") private Set learningPaths = new HashSet<>(); @OneToMany(mappedBy = "course", cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.LAZY) diff --git a/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java b/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java index c3897b5e7f46..e6df62ef4e36 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/competency/LearningPath.java @@ -19,6 +19,7 @@ @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) public class LearningPath extends DomainObject { + // number in [0, 100] representing the progress in percentage @Column(name = "progress") private int progress; diff --git a/src/main/java/de/tum/in/www1/artemis/repository/CompetencyProgressRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/CompetencyProgressRepository.java index 691a488d3f8c..6ec922248bb8 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/CompetencyProgressRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/CompetencyProgressRepository.java @@ -56,7 +56,7 @@ public interface CompetencyProgressRepository extends JpaRepository findAllByCompetencyIdsAndUserId(@Param("competencyIds") Set competencyIds, @Param("userId") Long userId); + Set findAllByCompetencyIdsAndUserId(@Param("competencyIds") Set competencyIds, @Param("userId") long userId); @Query(""" SELECT AVG(cp.confidence) diff --git a/src/main/java/de/tum/in/www1/artemis/service/CompetencyProgressService.java b/src/main/java/de/tum/in/www1/artemis/service/CompetencyProgressService.java index 7277448eb189..d2e3f5466a34 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/CompetencyProgressService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/CompetencyProgressService.java @@ -276,6 +276,7 @@ else if (learningObject instanceof Exercise exercise) { * @return True if the user mastered the competency, false otherwise */ public static boolean isMastered(@NotNull CompetencyProgress competencyProgress) { + // weight taken from client final double weight = 2.0 / 3.0; final double mastery = (1 - weight) * competencyProgress.getProgress() + weight * competencyProgress.getConfidence(); return mastery >= competencyProgress.getCompetency().getMasteryThreshold(); diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index 9a2fc5ca4922..8ecdbe94f3f5 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -27,6 +27,17 @@ import de.tum.in.www1.artemis.web.rest.dto.competency.NgxLearningPathDTO; import de.tum.in.www1.artemis.web.rest.util.PageUtil; +/** + * Service Implementation for managing Learning Paths. + *

    + * This includes + *

      + *
    • the generation of learning paths in courses,
    • + *
    • performing pageable searches for learning paths,
    • + *
    • generation of the Ngx representation of learning paths,
    • + *
    • and the computation of recommended learning objects for a specific learning path.
    • + *
    + */ @Service public class LearningPathService { @@ -401,13 +412,14 @@ public static String getDirectEdgeId(long competencyId) { * @param learningPath the learning path for which the recommendation should be computed * @return recommended learning object */ - public LearningObject getRecommendation(@NotNull LearningPath learningPath) { - return learningPath.getCompetencies().stream() + public Optional getRecommendation(@NotNull LearningPath learningPath) { + LearningObject learningObject = learningPath.getCompetencies().stream() .flatMap(competency -> Stream.concat( competency.getLectureUnits().stream() .filter(lectureUnit -> !lectureUnitRepository.findWithEagerCompletedUsersByIdElseThrow(lectureUnit.getId()).isCompletedFor(learningPath.getUser())), competency.getExercises().stream() .filter(exercise -> !exerciseRepository.findByIdWithStudentParticipationsElseThrow(exercise.getId()).isCompletedFor(learningPath.getUser())))) .findFirst().orElse(null); + return Optional.ofNullable(learningObject); } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java index 37db0dff1934..392a524051ad 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java @@ -8,7 +8,6 @@ import org.springframework.web.bind.annotation.*; import de.tum.in.www1.artemis.domain.Course; -import de.tum.in.www1.artemis.domain.LearningObject; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.competency.LearningPath; import de.tum.in.www1.artemis.domain.lecture.LectureUnit; @@ -158,16 +157,16 @@ public ResponseEntity getRecommendation(@PathVari if (!user.getId().equals(learningPath.getUser().getId())) { throw new AccessForbiddenException("You are not allowed to access another users learning path."); } - LearningObject recommendation = learningPathService.getRecommendation(learningPath); - if (recommendation == null) { + final var recommendation = learningPathService.getRecommendation(learningPath); + if (recommendation.isEmpty()) { return ResponseEntity.ok(new LearningPathRecommendationDTO(-1, -1, LearningPathRecommendationDTO.RecommendationType.EMPTY)); } - else if (recommendation instanceof LectureUnit lectureUnit) { - return ResponseEntity - .ok(new LearningPathRecommendationDTO(recommendation.getId(), lectureUnit.getLecture().getId(), LearningPathRecommendationDTO.RecommendationType.LECTURE_UNIT)); + else if (recommendation.get() instanceof LectureUnit lectureUnit) { + return ResponseEntity.ok(new LearningPathRecommendationDTO(recommendation.get().getId(), lectureUnit.getLecture().getId(), + LearningPathRecommendationDTO.RecommendationType.LECTURE_UNIT)); } else { - return ResponseEntity.ok(new LearningPathRecommendationDTO(recommendation.getId(), -1, LearningPathRecommendationDTO.RecommendationType.EXERCISE)); + return ResponseEntity.ok(new LearningPathRecommendationDTO(recommendation.get().getId(), -1, LearningPathRecommendationDTO.RecommendationType.EXERCISE)); } } } diff --git a/src/main/webapp/app/course/competencies/competency-management/competency-management.component.ts b/src/main/webapp/app/course/competencies/competency-management/competency-management.component.ts index eef1883389a7..c83c25c9e9b4 100644 --- a/src/main/webapp/app/course/competencies/competency-management/competency-management.component.ts +++ b/src/main/webapp/app/course/competencies/competency-management/competency-management.component.ts @@ -65,7 +65,7 @@ export class CompetencyManagementComponent implements OnInit, OnDestroy { ngOnInit(): void { this.showRelations = this.accountService.isAdmin(); // beta feature this.activatedRoute.parent!.params.subscribe((params) => { - this.courseId = +params['courseId']; + this.courseId = params['courseId']; if (this.courseId) { this.loadData(); } diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts index 04fe47a1f7e3..2e6d585502ac 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts @@ -114,7 +114,7 @@ export class LearningPathManagementComponent implements OnInit { this.content = { resultsOnPage: [], numberOfPages: 0 }; this.activatedRoute.parent!.params.subscribe((params) => { - this.courseId = +params['courseId']; + this.courseId = params['courseId']; if (this.courseId) { this.loadData(); } diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts index ea0756b434d1..c505e6fb9ff5 100644 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts @@ -45,7 +45,7 @@ export class LearningPathContainerComponent implements OnInit { ngOnInit() { if (!this.courseId) { this.activatedRoute.parent!.parent!.params.subscribe((params) => { - this.courseId = +params['courseId']; + this.courseId = params['courseId']; }); } this.learningPathService.getLearningPathId(this.courseId).subscribe((learningPathIdResponse) => { diff --git a/src/main/webapp/i18n/de/competency.json b/src/main/webapp/i18n/de/competency.json index 74db1b570c43..061eb65dc34e 100644 --- a/src/main/webapp/i18n/de/competency.json +++ b/src/main/webapp/i18n/de/competency.json @@ -155,7 +155,7 @@ "title": "Lernpfadmanagement", "isDisabled": "Lernpfade sind für diesen Kurs nicht aktiviert.", "enable": "Lernpfade aktivieren", - "enableHint": "Die Erstellung von Lernpfaden für alle Studierende kann einige Minuten dauern.", + "enableHint": "Die Erstellung von Lernpfaden für alle Studierende kann einen Moment dauern.", "search": "Suche nach Lernpfaden:", "table": { "name": "Name", @@ -165,7 +165,7 @@ } }, "sideBar": { - "hide": "Lernpfad ausbleden", + "hide": "Lernpfad ausblenden", "show": "Lernpfad einblenden", "header": "Lernpfad" }, diff --git a/src/main/webapp/i18n/en/competency.json b/src/main/webapp/i18n/en/competency.json index 3b7cb1d04568..caf0b4adaf4f 100644 --- a/src/main/webapp/i18n/en/competency.json +++ b/src/main/webapp/i18n/en/competency.json @@ -154,7 +154,7 @@ "title": "Learning Path Management", "isDisabled": "Learning Paths are currently disabled for this course.", "enable": "Enable Learning Paths", - "enableHint": "The creation of Learning Paths for every student may take a few minutes.", + "enableHint": "The creation of Learning Paths for every student may take a moment.", "search": "Search for Learning Path:", "table": { "name": "Name", diff --git a/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java b/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java index 9c116978e58e..49d087b295a2 100644 --- a/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java @@ -20,16 +20,16 @@ public class LearningPathUtilService { @Autowired - CourseRepository courseRepository; + private CourseRepository courseRepository; @Autowired - LearningPathService learningPathService; + private LearningPathService learningPathService; @Autowired - LearningPathRepository learningPathRepository; + private LearningPathRepository learningPathRepository; @Autowired - CompetencyRepository competencyRepository; + private CompetencyRepository competencyRepository; /** * Enable and generate learning paths for course. diff --git a/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java b/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java index e88e16c8d8f0..cb5f24895039 100644 --- a/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java +++ b/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java @@ -41,7 +41,6 @@ import de.tum.in.www1.artemis.assessment.ComplaintUtilService; import de.tum.in.www1.artemis.competency.CompetencyUtilService; -import de.tum.in.www1.artemis.competency.LearningPathUtilService; import de.tum.in.www1.artemis.config.Constants; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.competency.Competency; @@ -211,9 +210,6 @@ public class CourseTestService { @Autowired private TeamUtilService teamUtilService; - @Autowired - private LearningPathUtilService learningPathUtilService; - private static final int numberOfStudents = 8; private static final int numberOfTutors = 5; diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java index 011ce9c9bdc7..eb3a6d28457c 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java @@ -57,40 +57,40 @@ class LearningPathIntegrationTest extends AbstractSpringIntegrationBambooBitbuck private CourseUtilService courseUtilService; @Autowired - CompetencyUtilService competencyUtilService; + private CompetencyUtilService competencyUtilService; @Autowired - PageableSearchUtilService pageableSearchUtilService; + private PageableSearchUtilService pageableSearchUtilService; @Autowired - LearningPathRepository learningPathRepository; + private LearningPathRepository learningPathRepository; @Autowired - ExerciseUtilService exerciseUtilService; + private ExerciseUtilService exerciseUtilService; @Autowired - TextExerciseUtilService textExerciseUtilService; + private TextExerciseUtilService textExerciseUtilService; @Autowired - ParticipationUtilService participationUtilService; + private ParticipationUtilService participationUtilService; @Autowired - LectureRepository lectureRepository; + private LectureRepository lectureRepository; @Autowired - LectureUtilService lectureUtilService; + private LectureUtilService lectureUtilService; @Autowired - GradingCriterionRepository gradingCriterionRepository; + private GradingCriterionRepository gradingCriterionRepository; @Autowired - LectureUnitService lectureUnitService; + private LectureUnitService lectureUnitService; @Autowired - CompetencyProgressService competencyProgressService; + private CompetencyProgressService competencyProgressService; @Autowired - LearningPathUtilService learningPathUtilService; + private LearningPathUtilService learningPathUtilService; private Course course; @@ -100,7 +100,7 @@ class LearningPathIntegrationTest extends AbstractSpringIntegrationBambooBitbuck private TextUnit textUnit; - private final int NUMBER_OF_STUDENTS = 5; + private static final int NUMBER_OF_STUDENTS = 5; private static final String STUDENT_OF_COURSE = TEST_PREFIX + "student1"; diff --git a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java index 1588ae37b389..2c73f212a94e 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java @@ -25,7 +25,6 @@ import de.tum.in.www1.artemis.domain.lecture.LectureUnit; import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.lecture.LectureUtilService; -import de.tum.in.www1.artemis.repository.CompetencyRepository; import de.tum.in.www1.artemis.repository.LearningPathRepository; import de.tum.in.www1.artemis.security.SecurityUtils; import de.tum.in.www1.artemis.web.rest.dto.competency.NgxLearningPathDTO; @@ -33,28 +32,25 @@ class LearningPathServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { @Autowired - LearningPathService learningPathService; + private LearningPathService learningPathService; @Autowired - LearningPathUtilService learningPathUtilService; + private LearningPathUtilService learningPathUtilService; @Autowired - CourseUtilService courseUtilService; + private CourseUtilService courseUtilService; @Autowired - CompetencyUtilService competencyUtilService; + private CompetencyUtilService competencyUtilService; @Autowired - LearningPathRepository learningPathRepository; + private LearningPathRepository learningPathRepository; @Autowired - LectureUtilService lectureUtilService; + private LectureUtilService lectureUtilService; @Autowired - ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - CompetencyRepository competencyRepository; + private ProgrammingExerciseUtilService programmingExerciseUtilService; private Course course; @@ -284,7 +280,7 @@ void testGetRecommendationEmpty() { competencyUtilService.createCompetency(course); LearningPath learningPath = learningPathUtilService.createLearningPathInCourse(course); learningPath = learningPathRepository.findWithEagerCompetenciesAndLearningObjectsByIdElseThrow(learningPath.getId()); - assertThat(learningPathService.getRecommendation(learningPath)).isNull(); + assertThat(learningPathService.getRecommendation(learningPath)).isEmpty(); } @Test @@ -294,7 +290,7 @@ void testGetRecommendationNotEmpty() { competencyUtilService.linkLectureUnitToCompetency(competency, lectureUnit); LearningPath learningPath = learningPathUtilService.createLearningPathInCourse(course); learningPath = learningPathRepository.findWithEagerCompetenciesAndLearningObjectsByIdElseThrow(learningPath.getId()); - assertThat(learningPathService.getRecommendation(learningPath)).isNotNull(); + assertThat(learningPathService.getRecommendation(learningPath)).isPresent(); } } } From b52e296c44af447b38ef25220bf99bb367cbf9fb Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Sun, 30 Jul 2023 22:05:32 +0200 Subject: [PATCH 097/215] resolve merge conflict --- .../de/tum/in/www1/artemis/service/LearningPathServiceTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java index a2d38e6efc20..a28843d342db 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java @@ -27,6 +27,7 @@ import de.tum.in.www1.artemis.domain.lecture.LectureUnit; import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.lecture.LectureUtilService; +import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.LearningPathRepository; import de.tum.in.www1.artemis.security.SecurityUtils; import de.tum.in.www1.artemis.user.UserUtilService; From 52049e4a4ef57a221334c9711eb18f7325c600a8 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Sun, 30 Jul 2023 22:13:47 +0200 Subject: [PATCH 098/215] integrate Johannes' suggestions --- .../in/www1/artemis/repository/LearningPathRepository.java | 2 +- .../de/tum/in/www1/artemis/service/LearningPathService.java | 6 +++--- src/main/webapp/i18n/de/competency.json | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java index 4a9cca28bcd1..81d8e9929523 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java @@ -78,5 +78,5 @@ SELECT COUNT (learningPath) FROM LearningPath learningPath WHERE learningPath.course.id = :#{#courseId} """) - Long countByCourseId(@Param("courseId") long courseId); + long countByCourseId(@Param("courseId") long courseId); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index 47baec42ffce..d2b1aadb221b 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -435,10 +435,10 @@ public LearningPathHealthDTO getHealthStatusForCourse(Course course) { return new LearningPathHealthDTO(LearningPathHealthDTO.HealthStatus.DISABLED); } - var numberOfStudents = userRepository.countUserInGroup(course.getStudentGroupName()); - var numberOfLearningPaths = learningPathRepository.countByCourseId(course.getId()); + long numberOfStudents = userRepository.countUserInGroup(course.getStudentGroupName()); + long numberOfLearningPaths = learningPathRepository.countByCourseId(course.getId()); - if (numberOfStudents.equals(numberOfLearningPaths)) { + if (numberOfStudents == numberOfLearningPaths) { return new LearningPathHealthDTO(LearningPathHealthDTO.HealthStatus.OK); } else { diff --git a/src/main/webapp/i18n/de/competency.json b/src/main/webapp/i18n/de/competency.json index 78e22fc4188e..c71470bfdea6 100644 --- a/src/main/webapp/i18n/de/competency.json +++ b/src/main/webapp/i18n/de/competency.json @@ -172,8 +172,8 @@ }, "progressNav": { "header": "Lernpfad", - "refresh": "Refresh", - "center": "Center view" + "refresh": "Aktualisieren", + "center": "Zentrieren" } }, "sideBar": { From b246cfc45320884f81476866a2e96b014354a554 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Mon, 31 Jul 2023 16:52:01 +0200 Subject: [PATCH 099/215] Update LearningPathServiceTest.java --- .../tum/in/www1/artemis/service/LearningPathServiceTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java index 2c73f212a94e..c7f600efd0a9 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java @@ -21,7 +21,6 @@ import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.competency.CompetencyRelation; import de.tum.in.www1.artemis.domain.competency.LearningPath; -import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; import de.tum.in.www1.artemis.domain.lecture.LectureUnit; import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.lecture.LectureUtilService; @@ -105,7 +104,7 @@ void testCompetencyWithLectureUnitAndExercise() { var competency = competencyUtilService.createCompetency(course); final var lectureUnit = lectureUtilService.createTextUnit(); competencyUtilService.linkLectureUnitToCompetency(competency, lectureUnit); - final var exercise = programmingExerciseUtilService.addProgrammingExerciseToCourse(course, false, false, ProgrammingLanguage.JAVA, "Some Title", "someshortname"); + final var exercise = programmingExerciseUtilService.addProgrammingExerciseToCourse(course, false); competencyUtilService.linkExerciseToCompetency(competency, exercise); final var startNodeId = LearningPathService.getCompetencyStartNodeId(competency.getId()); final var endNodeId = LearningPathService.getCompetencyEndNodeId(competency.getId()); From bfc0eee0a1d8289113a23c1951610039c1242f94 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Mon, 31 Jul 2023 18:51:08 +0200 Subject: [PATCH 100/215] fix enable learning paths button tooltip --- .../learning-path-management.component.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html index 4d85780caa13..f1634b94c66c 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html @@ -8,7 +8,7 @@

    Learning Path Management

    Disabled -
    +
    From 0afac6f5ec08ad1acc30f2e520a25c466e854f85 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Mon, 31 Jul 2023 18:57:31 +0200 Subject: [PATCH 101/215] fix position of learning paths tab in course managment --- .../course-management-tab-bar.component.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html b/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html index 7d9f4770ea43..22d9121ccc06 100644 --- a/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html +++ b/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html @@ -27,10 +27,6 @@ Messages - - - Learning Paths - Competencies + + + Learning Paths + Assessment From 5e81ccefc03759f6c6d08a9206a61d8da3ddd15b Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Mon, 31 Jul 2023 19:00:40 +0200 Subject: [PATCH 102/215] fix position of learning path tab in course overview --- .../webapp/app/overview/course-overview.component.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/webapp/app/overview/course-overview.component.html b/src/main/webapp/app/overview/course-overview.component.html index 2d4ae93b811d..b1bbce889f7e 100644 --- a/src/main/webapp/app/overview/course-overview.component.html +++ b/src/main/webapp/app/overview/course-overview.component.html @@ -15,10 +15,6 @@ Lectures - - - Learning Path - Statistics @@ -55,6 +51,10 @@ Competencies + + + Learning Path +
    -
    +
    {{ 'artemisApp.learningPath.manageLearningPaths.health.missing.title' | artemisTranslate }}

    {{ 'artemisApp.learningPath.manageLearningPaths.health.missing.body' | artemisTranslate }}

    From 469bf356032b72ea198dc5f93a2d5e5228837556 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Mon, 31 Jul 2023 19:07:00 +0200 Subject: [PATCH 104/215] fix translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Johannes Stöhr <38322605+JohannesStoehr@users.noreply.github.com> --- src/main/webapp/i18n/en/competency.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/i18n/en/competency.json b/src/main/webapp/i18n/en/competency.json index c3be27fb29a6..5c08d79245a2 100644 --- a/src/main/webapp/i18n/en/competency.json +++ b/src/main/webapp/i18n/en/competency.json @@ -155,7 +155,7 @@ "health": { "missing": { "title": "Missing Learning Paths", - "body": "Some student's have not generated their Learning Paths yet. This is not critical. Their Learning Paths will be created once they request their learning path for the first time.", + "body": "Some students have not generated their learning paths yet. This is not critical. Their learning paths will be created once they request their learning path for the first time.", "action": "Generate", "hint": "Generate missing Learning Paths" } From 74509f839e0ec123839e39edb51b5f980b46510f Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Mon, 31 Jul 2023 19:23:09 +0200 Subject: [PATCH 105/215] fix health status stuck --- .../in/www1/artemis/repository/LearningPathRepository.java | 4 ++-- .../de/tum/in/www1/artemis/service/LearningPathService.java | 2 +- .../learning-path-management.component.html | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java index 81d8e9929523..9e18387ce318 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java @@ -76,7 +76,7 @@ default LearningPath findWithEagerCompetenciesAndLearningObjectsAndCompletedUser @Query(""" SELECT COUNT (learningPath) FROM LearningPath learningPath - WHERE learningPath.course.id = :#{#courseId} + WHERE learningPath.course.id = :courseId AND learningPath.user.isDeleted = false AND learningPath.course.studentGroupName MEMBER OF learningPath.user.groups """) - long countByCourseId(@Param("courseId") long courseId); + long countLearningPathsOfEnrolledStudentsInCourse(@Param("courseId") long courseId); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index d2b1aadb221b..84ae45467d4c 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -436,7 +436,7 @@ public LearningPathHealthDTO getHealthStatusForCourse(Course course) { } long numberOfStudents = userRepository.countUserInGroup(course.getStudentGroupName()); - long numberOfLearningPaths = learningPathRepository.countByCourseId(course.getId()); + long numberOfLearningPaths = learningPathRepository.countLearningPathsOfEnrolledStudentsInCourse(course.getId()); if (numberOfStudents == numberOfLearningPaths) { return new LearningPathHealthDTO(LearningPathHealthDTO.HealthStatus.OK); diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html index c7360be50569..9447ba7680ea 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html @@ -22,7 +22,7 @@

    Learning Pa

    -
    +
    {{ 'artemisApp.learningPath.manageLearningPaths.health.missing.title' | artemisTranslate }}

    {{ 'artemisApp.learningPath.manageLearningPaths.health.missing.body' | artemisTranslate }}

    From 77efd3ac847f67abe5bc8365f86c5b1067ff21b7 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Mon, 31 Jul 2023 19:46:54 +0200 Subject: [PATCH 106/215] generate learning path when student is added by instructor --- .../in/www1/artemis/repository/CourseRepository.java | 8 ++++++++ .../tum/in/www1/artemis/web/rest/CourseResource.java | 10 +++++++++- .../tum/in/www1/artemis/course/CourseTestService.java | 4 +++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java index 9c2dd9fc6b7d..5b8e5f1422f9 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java @@ -131,6 +131,9 @@ SELECT CASE WHEN (count(c) > 0) THEN true ELSE false END @EntityGraph(type = LOAD, attributePaths = { "competencies", "prerequisites" }) Optional findWithEagerCompetenciesById(long courseId); + @EntityGraph(type = LOAD, attributePaths = { "learningPaths" }) + Optional findWithEagerLearningPathsById(long courseId); + @EntityGraph(type = LOAD, attributePaths = { "competencies", "learningPaths", "learningPaths.competencies" }) Optional findWithEagerLearningPathsAndCompetenciesById(long courseId); @@ -379,6 +382,11 @@ default Course findWithEagerCompetenciesByIdElseThrow(long courseId) { return findWithEagerCompetenciesById(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId)); } + @NotNull + default Course findWithEagerLearningPathsByIdElseThrow(long courseId) { + return findWithEagerLearningPathsById(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId)); + } + @NotNull default Course findWithEagerLearningPathsAndCompetenciesByIdElseThrow(long courseId) { return findWithEagerLearningPathsAndCompetenciesById(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId)); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java index 905b332ff37b..feaf469135a8 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java @@ -113,12 +113,15 @@ public class CourseResource { private final ChannelService channelService; + private final LearningPathService learningPathService; + public CourseResource(UserRepository userRepository, CourseService courseService, CourseRepository courseRepository, ExerciseService exerciseService, OAuth2JWKSService oAuth2JWKSService, OnlineCourseConfigurationService onlineCourseConfigurationService, AuthorizationCheckService authCheckService, TutorParticipationRepository tutorParticipationRepository, SubmissionService submissionService, Optional optionalVcsUserManagementService, AssessmentDashboardService assessmentDashboardService, ExerciseRepository exerciseRepository, Optional optionalCiUserManagementService, FileService fileService, TutorialGroupsConfigurationService tutorialGroupsConfigurationService, GradingScaleService gradingScaleService, - CourseScoreCalculationService courseScoreCalculationService, GradingScaleRepository gradingScaleRepository, ChannelService channelService) { + CourseScoreCalculationService courseScoreCalculationService, GradingScaleRepository gradingScaleRepository, ChannelService channelService, + LearningPathService learningPathService) { this.courseService = courseService; this.courseRepository = courseRepository; this.exerciseService = exerciseService; @@ -138,6 +141,7 @@ public CourseResource(UserRepository userRepository, CourseService courseService this.courseScoreCalculationService = courseScoreCalculationService; this.gradingScaleRepository = gradingScaleRepository; this.channelService = channelService; + this.learningPathService = learningPathService; } /** @@ -1035,6 +1039,10 @@ public ResponseEntity addUserToCourseGroup(String userLogin, User instruct } courseService.addUserToGroup(userToAddToGroup.get(), group, role); channelService.registerUserToDefaultChannels(userToAddToGroup.get(), group, role); + if (role == Role.STUDENT && course.getLearningPathsEnabled()) { + Course courseWithCompetencies = courseRepository.findWithEagerCompetenciesByIdElseThrow(course.getId()); + learningPathService.generateLearningPathForUser(courseWithCompetencies, userToAddToGroup.get()); + } return ResponseEntity.ok().body(null); } else { diff --git a/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java b/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java index cb5f24895039..6f85679b4ae3 100644 --- a/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java +++ b/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java @@ -1733,9 +1733,11 @@ public void testAddStudentOrTutorOrEditorOrInstructorToCourse() throws Exception adjustUserGroupsToCustomGroups(); Course course = CourseFactory.generateCourse(null, null, null, new HashSet<>(), userPrefix + "student", userPrefix + "tutor", userPrefix + "editor", userPrefix + "instructor"); + course.setLearningPathsEnabled(true); course = courseRepo.save(course); testAddStudentOrTutorOrEditorOrInstructorToCourse(course, HttpStatus.OK); - + course = courseRepo.findWithEagerLearningPathsByIdElseThrow(course.getId()); + assertThat(course.getLearningPaths()).isNotEmpty(); // TODO check that the roles have changed accordingly } From 8f76721683a1c26210f75b4def4737191c8bdb19 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Mon, 31 Jul 2023 20:15:09 +0200 Subject: [PATCH 107/215] Update CourseService.java --- .../java/de/tum/in/www1/artemis/service/CourseService.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/de/tum/in/www1/artemis/service/CourseService.java b/src/main/java/de/tum/in/www1/artemis/service/CourseService.java index dd3242d2df14..f26c1b5daf26 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/CourseService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/CourseService.java @@ -496,6 +496,9 @@ public void enrollUserForCourseOrThrow(User user, Course course) { */ public List registerUsersForCourseGroup(Long courseId, List studentDTOs, String courseGroup) { var course = courseRepository.findByIdElseThrow(courseId); + if (course.getLearningPathsEnabled()) { + course = courseRepository.findWithEagerCompetenciesByIdElseThrow(course.getId()); + } String courseGroupName = course.defineCourseGroupName(courseGroup); Role courseGroupRole = Role.fromString(courseGroup); List notFoundStudentsDTOs = new ArrayList<>(); @@ -507,6 +510,9 @@ public List registerUsersForCourseGroup(Long courseId, List Date: Tue, 1 Aug 2023 00:59:52 +0200 Subject: [PATCH 108/215] fix breadcrumbs --- src/main/webapp/app/shared/layouts/navbar/navbar.component.ts | 2 ++ src/main/webapp/i18n/de/competency.json | 4 ++++ src/main/webapp/i18n/en/competency.json | 4 ++++ 3 files changed, 10 insertions(+) diff --git a/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts b/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts index 417e4a0e648a..b0bb13855a86 100644 --- a/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts +++ b/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts @@ -337,6 +337,8 @@ export class NavbarComponent implements OnInit, OnDestroy { lectures: 'artemisApp.courseOverview.menu.lectures', competencies: 'artemisApp.courseOverview.menu.competencies', learning_path: 'artemisApp.courseOverview.menu.learningPath', + lecture_unit: 'artemisApp.learningPath.breadcrumbs.lectureUnit', + exercise: 'artemisApp.learningPath.breadcrumbs.exercise', statistics: 'artemisApp.courseOverview.menu.statistics', discussion: 'artemisApp.metis.communication.label', messages: 'artemisApp.conversationsLayout.breadCrumbLabel', diff --git a/src/main/webapp/i18n/de/competency.json b/src/main/webapp/i18n/de/competency.json index 061eb65dc34e..201f8fed2de4 100644 --- a/src/main/webapp/i18n/de/competency.json +++ b/src/main/webapp/i18n/de/competency.json @@ -151,6 +151,10 @@ }, "learningPath": { "learningPathButton": "Lernpfade", + "breadcrumbs": { + "lectureUnit": "Vorlesungseinheit", + "exercise": "Aufgabe" + }, "manageLearningPaths": { "title": "Lernpfadmanagement", "isDisabled": "Lernpfade sind für diesen Kurs nicht aktiviert.", diff --git a/src/main/webapp/i18n/en/competency.json b/src/main/webapp/i18n/en/competency.json index caf0b4adaf4f..22a39cf4cc52 100644 --- a/src/main/webapp/i18n/en/competency.json +++ b/src/main/webapp/i18n/en/competency.json @@ -150,6 +150,10 @@ }, "learningPath": { "learningPathButton": "Learning Paths", + "breadcrumbs": { + "lectureUnit": "Lecture Unit", + "exercise": "Exercise" + }, "manageLearningPaths": { "title": "Learning Path Management", "isDisabled": "Learning Paths are currently disabled for this course.", From 02a0c6ea265cd8b127c98c0ba102d0c7bf5711d1 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Tue, 1 Aug 2023 11:20:35 +0200 Subject: [PATCH 109/215] refactor participation history --- .../learning-paths/learning-paths.module.ts | 1 + .../learning-path-container.component.html | 2 +- .../learning-path-container.component.ts | 28 ++-- .../learning-path-history-storage.service.ts | 85 ++++++++++++ src/main/webapp/i18n/de/competency.json | 6 +- src/main/webapp/i18n/en/competency.json | 10 +- .../learning-path-container.component.spec.ts | 29 +++- ...rning-path-history-storage.service.spec.ts | 127 ++++++++++++++++++ 8 files changed, 265 insertions(+), 23 deletions(-) create mode 100644 src/main/webapp/app/course/learning-paths/participate/learning-path-history-storage.service.ts create mode 100644 src/test/javascript/spec/service/learning-path-history-storage.service.spec.ts diff --git a/src/main/webapp/app/course/learning-paths/learning-paths.module.ts b/src/main/webapp/app/course/learning-paths/learning-paths.module.ts index 7f03e4c6e2f0..a4e62c655efe 100644 --- a/src/main/webapp/app/course/learning-paths/learning-paths.module.ts +++ b/src/main/webapp/app/course/learning-paths/learning-paths.module.ts @@ -13,6 +13,7 @@ import { LearningPathGraphNodeComponent } from 'app/course/learning-paths/learni import { NgxGraphModule } from '@swimlane/ngx-graph'; import { ArtemisLectureUnitsModule } from 'app/overview/course-lectures/lecture-units.module'; import { LearningPathProgressModalComponent } from 'app/course/learning-paths/learning-path-management/learning-path-progress-modal.component'; +import { LearningPathHistoryStorageService } from 'app/course/learning-paths/participate/learning-path-history-storage.service'; const routes: Routes = [ { diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html index ee54c1faa122..40389ae34f04 100644 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html @@ -11,7 +11,7 @@
    - - - -
    -
    diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-nav.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-nav.component.ts deleted file mode 100644 index c050632cc577..000000000000 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-nav.component.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { faArrowsRotate, faArrowsToEye, faXmark } from '@fortawesome/free-solid-svg-icons'; -import { LearningPathPageableSearchDTO } from 'app/entities/competency/learning-path.model'; - -@Component({ - selector: 'jhi-learning-path-progress-nav', - templateUrl: './learning-path-progress-nav.component.html', -}) -export class LearningPathProgressNavComponent { - @Input() learningPath: LearningPathPageableSearchDTO; - @Output() onRefresh: EventEmitter = new EventEmitter(); - @Output() onCenterView: EventEmitter = new EventEmitter(); - @Output() onClose: EventEmitter = new EventEmitter(); - - // icons - faXmark = faXmark; - faArrowsToEye = faArrowsToEye; - faArrowsRotate = faArrowsRotate; -} diff --git a/src/main/webapp/app/course/learning-paths/learning-path.service.ts b/src/main/webapp/app/course/learning-paths/learning-path.service.ts index 7fc0e33a1a3c..ece1bc6b204a 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path.service.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path.service.ts @@ -1,8 +1,6 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { HttpClient, HttpResponse } from '@angular/common/http'; -import { LearningPathRecommendationDTO, NgxLearningPathDTO } from 'app/entities/competency/learning-path.model'; -import { map } from 'rxjs/operators'; import { LearningPathHealthDTO } from 'app/entities/competency/learning-path-health.model'; @Injectable({ providedIn: 'root' }) @@ -19,31 +17,6 @@ export class LearningPathService { return this.httpClient.put(`${this.resourceURL}/courses/${courseId}/learning-paths/generate-missing`, null, { observe: 'response' }); } - getLearningPathId(courseId: number) { - return this.httpClient.get(`${this.resourceURL}/courses/${courseId}/learning-path-id`, { observe: 'response' }); - } - - getNgxLearningPath(learningPathId: number): Observable> { - return this.httpClient.get(`${this.resourceURL}/learning-path/${learningPathId}`, { observe: 'response' }).pipe( - map((ngxLearningPathResponse) => { - if (!ngxLearningPathResponse.body!.nodes) { - ngxLearningPathResponse.body!.nodes = []; - } - if (!ngxLearningPathResponse.body!.edges) { - ngxLearningPathResponse.body!.edges = []; - } - if (!ngxLearningPathResponse.body!.clusters) { - ngxLearningPathResponse.body!.clusters = []; - } - return ngxLearningPathResponse; - }), - ); - } - - getRecommendation(learningPathId: number) { - return this.httpClient.get(`${this.resourceURL}/learning-path/${learningPathId}/recommendation`, { observe: 'response' }); - } - getHealthStatusForCourse(courseId: number) { return this.httpClient.get(`${this.resourceURL}/courses/${courseId}/learning-path-health`, { observe: 'response' }); } diff --git a/src/main/webapp/app/course/learning-paths/learning-paths.module.ts b/src/main/webapp/app/course/learning-paths/learning-paths.module.ts index f44050de7416..4d1a254a674f 100644 --- a/src/main/webapp/app/course/learning-paths/learning-paths.module.ts +++ b/src/main/webapp/app/course/learning-paths/learning-paths.module.ts @@ -1,87 +1,12 @@ import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { LearningPathManagementComponent } from 'app/course/learning-paths/learning-path-management/learning-path-management.component'; -import { LearningPathContainerComponent } from 'app/course/learning-paths/participate/learning-path-container.component'; -import { Authority } from 'app/shared/constants/authority.constants'; -import { UserRouteAccessService } from 'app/core/auth/user-route-access-service'; -import { LearningPathGraphSidebarComponent } from 'app/course/learning-paths/participate/learning-path-graph-sidebar.component'; -import { LearningPathGraphComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph.component'; -import { LearningPathGraphNodeComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph-node.component'; -import { NgxGraphModule } from '@swimlane/ngx-graph'; -import { ArtemisLectureUnitsModule } from 'app/overview/course-lectures/lecture-units.module'; -import { LearningPathProgressModalComponent } from 'app/course/learning-paths/learning-path-management/learning-path-progress-modal.component'; -import { LearningPathProgressNavComponent } from 'app/course/learning-paths/learning-path-management/learning-path-progress-nav.component'; -import { CompetencyNodeDetailsComponent } from 'app/course/learning-paths/learning-path-graph/node-details/competency-node-details.component'; -import { ArtemisCompetenciesModule } from 'app/course/competencies/competency.module'; -import { LectureUnitNodeDetailsComponent } from 'app/course/learning-paths/learning-path-graph/node-details/lecture-unit-node-details.component'; -import { ExerciseNodeDetailsComponent } from 'app/course/learning-paths/learning-path-graph/node-details/exercise-node-details.component'; - -const routes: Routes = [ - { - path: '', - component: LearningPathContainerComponent, - data: { - authorities: [Authority.USER], - pageTitle: 'overview.learningPath', - }, - canActivate: [UserRouteAccessService], - children: [ - { - path: 'lecture-unit', - pathMatch: 'full', - children: [ - { - path: '', - pathMatch: 'full', - loadChildren: () => - import('app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.module').then( - (m) => m.ArtemisLearningPathLectureUnitViewModule, - ), - }, - ], - }, - { - path: 'exercise', - pathMatch: 'full', - children: [ - { - path: '', - pathMatch: 'full', - loadChildren: () => import('app/overview/exercise-details/course-exercise-details.module').then((m) => m.CourseExerciseDetailsModule), - }, - ], - }, - ], - }, -]; @NgModule({ - imports: [ - ArtemisSharedModule, - FormsModule, - ReactiveFormsModule, - ArtemisSharedComponentModule, - NgxGraphModule, - RouterModule.forChild(routes), - ArtemisLectureUnitsModule, - ArtemisCompetenciesModule, - ], - declarations: [ - LearningPathContainerComponent, - LearningPathManagementComponent, - LearningPathProgressModalComponent, - LearningPathProgressNavComponent, - LearningPathGraphSidebarComponent, - LearningPathGraphComponent, - LearningPathGraphNodeComponent, - LearningPathProgressModalComponent, - CompetencyNodeDetailsComponent, - LectureUnitNodeDetailsComponent, - ExerciseNodeDetailsComponent, - ], - exports: [LearningPathContainerComponent], + imports: [ArtemisSharedModule, FormsModule, ReactiveFormsModule, ArtemisSharedComponentModule], + declarations: [LearningPathManagementComponent], + exports: [], }) export class ArtemisLearningPathsModule {} diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html deleted file mode 100644 index 9c25931bcb99..000000000000 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html +++ /dev/null @@ -1,24 +0,0 @@ -
    -
    - -
    -
    -
    -
    - No task selected -
    - -
    -
    -
    - - diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.scss b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.scss deleted file mode 100644 index 7b9a664c9b4b..000000000000 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.scss +++ /dev/null @@ -1,13 +0,0 @@ -.main-container { - width: 100%; - margin-left: 0; -} - -.course-info-bar { - margin: 0 !important; -} - -.tab-bar-exercise-details { - margin-left: 0 !important; - margin-right: 0 !important; -} diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts deleted file mode 100644 index 2e4277c408cb..000000000000 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; -import { faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons'; -import { Exercise } from 'app/entities/exercise.model'; -import { LectureUnit } from 'app/entities/lecture-unit/lectureUnit.model'; -import { Lecture } from 'app/entities/lecture.model'; -import { LearningPathService } from 'app/course/learning-paths/learning-path.service'; -import { NgxLearningPathNode, NodeType, RecommendationType } from 'app/entities/competency/learning-path.model'; -import { LectureService } from 'app/lecture/lecture.service'; -import { onError } from 'app/shared/util/global.utils'; -import { HttpErrorResponse } from '@angular/common/http'; -import { AlertService } from 'app/core/util/alert.service'; -import { LearningPathLectureUnitViewComponent } from 'app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component'; -import { CourseExerciseDetailsComponent } from 'app/overview/exercise-details/course-exercise-details.component'; -import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; -import { ExerciseEntry, LearningPathHistoryStorageService, LectureUnitEntry } from 'app/course/learning-paths/participate/learning-path-history-storage.service'; - -@Component({ - selector: 'jhi-learning-path-container', - styleUrls: ['./learning-path-container.component.scss'], - templateUrl: './learning-path-container.component.html', - encapsulation: ViewEncapsulation.None, -}) -export class LearningPathContainerComponent implements OnInit { - @Input() courseId: number; - learningPathId: number; - - learningObjectId: number; - lectureId?: number; - lecture: Lecture | undefined; - lectureUnit: LectureUnit | undefined; - exercise: Exercise | undefined; - - // icons - faChevronLeft = faChevronLeft; - faChevronRight = faChevronRight; - - constructor( - private router: Router, - private activatedRoute: ActivatedRoute, - private alertService: AlertService, - private learningPathService: LearningPathService, - private lectureService: LectureService, - private exerciseService: ExerciseService, - private learningPathHistoryStorageService: LearningPathHistoryStorageService, - ) {} - - ngOnInit() { - if (!this.courseId) { - this.activatedRoute.parent!.parent!.params.subscribe((params) => { - this.courseId = params['courseId']; - }); - } - this.learningPathService.getLearningPathId(this.courseId).subscribe((learningPathIdResponse) => { - this.learningPathId = learningPathIdResponse.body!; - }); - } - - onNextTask() { - if (this.lectureUnit?.id) { - this.learningPathHistoryStorageService.storeLectureUnit(this.learningPathId, this.lectureId!, this.lectureUnit.id); - } else if (this.exercise?.id) { - this.learningPathHistoryStorageService.storeExercise(this.learningPathId, this.exercise.id); - } - this.undefineAll(); - this.learningPathService.getRecommendation(this.learningPathId).subscribe((recommendationResponse) => { - const recommendation = recommendationResponse.body!; - this.learningObjectId = recommendation.learningObjectId; - this.lectureId = recommendation.lectureId; - if (recommendation.type == RecommendationType.LECTURE_UNIT) { - this.loadLectureUnit(); - } else if (recommendation.type === RecommendationType.EXERCISE) { - this.loadExercise(); - } - }); - } - - undefineAll() { - this.lecture = undefined; - this.lectureUnit = undefined; - this.exercise = undefined; - } - - onPrevTask() { - this.undefineAll(); - if (this.learningPathHistoryStorageService.hasPrevious(this.learningPathId)) { - const entry = this.learningPathHistoryStorageService.getPrevious(this.learningPathId); - if (entry instanceof LectureUnitEntry) { - this.learningObjectId = entry.lectureUnitId; - this.lectureId = entry.lectureId; - this.loadLectureUnit(); - } else if (entry instanceof ExerciseEntry) { - this.learningObjectId = entry.exerciseId; - this.loadExercise(); - } - } - } - - loadLectureUnit() { - this.lectureService.findWithDetails(this.lectureId!).subscribe({ - next: (findLectureResult) => { - this.lecture = findLectureResult.body!; - if (this.lecture?.lectureUnits) { - this.lectureUnit = this.lecture.lectureUnits.find((lectureUnit) => lectureUnit.id === this.learningObjectId); - } - }, - error: (errorResponse: HttpErrorResponse) => onError(this.alertService, errorResponse), - }); - this.router.navigate(['lecture-unit'], { relativeTo: this.activatedRoute }); - } - - loadExercise() { - this.exerciseService.getExerciseDetails(this.learningObjectId).subscribe({ - next: (exerciseResponse) => { - this.exercise = exerciseResponse.body!; - }, - error: (errorResponse: HttpErrorResponse) => onError(this.alertService, errorResponse), - }); - this.router.navigate(['exercise'], { relativeTo: this.activatedRoute }); - } - - /** - * This function gets called if the router outlet gets activated. This is - * used only for the LearningPathLectureUnitViewComponent - * @param instance The component instance - */ - onChildActivate(instance: LearningPathLectureUnitViewComponent | CourseExerciseDetailsComponent) { - if (instance instanceof LearningPathLectureUnitViewComponent) { - this.setupLectureUnitView(instance); - } else { - this.setupExerciseView(instance); - } - } - - setupLectureUnitView(instance: LearningPathLectureUnitViewComponent) { - if (this.lecture) { - instance.lecture = this.lecture; - instance.lectureUnit = this.lectureUnit!; - } - } - - setupExerciseView(instance: CourseExerciseDetailsComponent) { - if (this.exercise) { - instance.courseId = this.courseId; - instance.exerciseId = this.learningObjectId; - } - } - - hasPrevious() { - return this.learningPathHistoryStorageService.hasPrevious(this.learningPathId); - } - - onNodeClicked(node: NgxLearningPathNode) { - if (node.type === NodeType.LECTURE_UNIT || node.type === NodeType.EXERCISE) { - if (this.lectureUnit?.id) { - this.learningPathHistoryStorageService.storeLectureUnit(this.learningPathId, this.lectureId!, this.lectureUnit.id); - } else if (this.exercise?.id) { - this.learningPathHistoryStorageService.storeExercise(this.learningPathId, this.exercise.id); - } - this.undefineAll(); - this.learningObjectId = node.linkedResource!; - this.lectureId = node.linkedResourceParent; - if (node.type === NodeType.LECTURE_UNIT) { - this.loadLectureUnit(); - } else if (node.type === NodeType.EXERCISE) { - this.loadExercise(); - } - } - } -} diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.html b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.html deleted file mode 100644 index 0c64d339f083..000000000000 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.html +++ /dev/null @@ -1,36 +0,0 @@ -
    -
    -
    - -
    -

    - - {{ 'artemisApp.learningPath.sideBar.header' | artemisTranslate }} -

    -
    - -
    -
    - -
    - -
    -
    -
    - -
    -
    - -
    - - Learning Path - - -
    -
    diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.scss b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.scss deleted file mode 100644 index ee0763fe5218..000000000000 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.scss +++ /dev/null @@ -1,77 +0,0 @@ -@import 'src/main/webapp/content/scss/artemis-variables'; - -$draggable-width: 15px; -$graph-min-width: 215px; - -.learning-path-sidebar { - .expanded-graph { - display: flex; - width: calc(#{$draggable-width} + #{$graph-min-width}); - min-height: 500px; - margin-left: auto; - - .draggable-right { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - min-width: $draggable-width; - } - - .card { - width: inherit; - min-width: $graph-min-width; - - .card-header { - display: inline-flex; - justify-content: space-between; - align-items: center; - cursor: pointer; - - .card-title { - display: flex; - } - - .row > .col-auto:last-child { - display: flex; - flex-direction: column; - justify-content: center; - } - } - - .card-body { - padding: 0; - } - } - } - - .collapsed-graph { - display: flex; - width: 38px; - justify-content: space-between; - flex-flow: column; - cursor: pointer; - - span { - writing-mode: vertical-lr; - transform: rotate(180deg); - margin: auto; - } - - .expand-graph-icon { - padding-top: 0.5rem; - padding-bottom: 0.5rem; - place-self: center; - } - } - - @media screen and (max-width: 992px) { - .expanded-graph { - width: 94vw; - - .draggable-right { - display: none; - } - } - } -} diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.ts b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.ts deleted file mode 100644 index 676656f48bca..000000000000 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { AfterViewInit, Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; -import interact from 'interactjs'; -import { faChevronLeft, faChevronRight, faGripLinesVertical, faNetworkWired } from '@fortawesome/free-solid-svg-icons'; -import { LearningPathGraphComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph.component'; -import { NgxLearningPathNode } from 'app/entities/competency/learning-path.model'; - -@Component({ - selector: 'jhi-learning-path-graph-sidebar', - styleUrls: ['./learning-path-graph-sidebar.component.scss'], - templateUrl: './learning-path-graph-sidebar.component.html', -}) -export class LearningPathGraphSidebarComponent implements AfterViewInit { - @Input() courseId: number; - @Input() learningPathId: number; - collapsed: boolean; - // Icons - faChevronLeft = faChevronLeft; - faChevronRight = faChevronRight; - faGripLinesVertical = faGripLinesVertical; - faNetworkWired = faNetworkWired; - - @ViewChild(`learningPathGraphComponent`, { static: false }) - learningPathGraphComponent: LearningPathGraphComponent; - - @Output() nodeClicked: EventEmitter = new EventEmitter(); - - ngAfterViewInit(): void { - // allows the sidebar to be resized towards the right-hand side - interact('.expanded-graph') - .resizable({ - edges: { left: false, right: '.draggable-right', bottom: false, top: false }, - modifiers: [ - // Set maximum width of the sidebar - interact.modifiers!.restrictSize({ - min: { width: 230, height: 0 }, - max: { width: 500, height: 4000 }, - }), - ], - inertia: true, - }) - .on('resizestart', (event: any) => { - event.target.classList.add('card-resizable'); - }) - .on('resizeend', (event: any) => { - event.target.classList.remove('card-resizable'); - this.learningPathGraphComponent.onResize(); - }) - .on('resizemove', (event: any) => { - const target = event.target; - target.style.width = event.rect.width + 'px'; - }); - } -} diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-history-storage.service.ts b/src/main/webapp/app/course/learning-paths/participate/learning-path-history-storage.service.ts deleted file mode 100644 index cc3baab7cc6c..000000000000 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-history-storage.service.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Injectable } from '@angular/core'; - -/** - * This service is used to store the histories of learning path participation for the currently logged-in user. - */ -@Injectable({ providedIn: 'root' }) -export class LearningPathHistoryStorageService { - private readonly learningPathHistories: Map = new Map(); - - /** - * Stores the lecture unit in the learning path's history. - * - * @param learningPathId the id of the learning path to which the new entry should be added - * @param lectureId the id of the lecture, the lecture unit belongs to - * @param lectureUnitId the id of the lecture unit - */ - storeLectureUnit(learningPathId: number, lectureId: number, lectureUnitId: number) { - this.store(learningPathId, new LectureUnitEntry(lectureId, lectureUnitId)); - } - - /** - * Stores the exercise in the learning path's history. - * - * @param learningPathId the id of the learning path to which the new entry should be added - * @param exerciseId the id of the exercise - */ - storeExercise(learningPathId: number, exerciseId: number) { - this.store(learningPathId, new ExerciseEntry(exerciseId)); - } - private store(learningPathId: number, entry: HistoryEntry) { - if (!entry) { - return; - } - if (!this.learningPathHistories.has(learningPathId)) { - this.learningPathHistories.set(learningPathId, []); - } - this.learningPathHistories.get(learningPathId)!.push(entry); - } - - /** - * Returns if the learning path's history stores at least one entry. - * - * @param learningPathId the id of the learning path for which the history should be checked - */ - hasPrevious(learningPathId: number): boolean { - if (this.learningPathHistories.has(learningPathId)) { - return this.learningPathHistories.get(learningPathId)!.length !== 0; - } - return false; - } - - /** - * Gets and removes the latest stored entry from the learning path's history. - * - * @param learningPathId - */ - getPrevious(learningPathId: number) { - if (!this.hasPrevious(learningPathId)) { - return null; - } - return this.learningPathHistories.get(learningPathId)!.pop(); - } -} - -export abstract class HistoryEntry {} - -export class LectureUnitEntry extends HistoryEntry { - lectureUnitId: number; - lectureId: number; - - constructor(lectureId: number, lectureUnitId: number) { - super(); - this.lectureId = lectureId; - this.lectureUnitId = lectureUnitId; - } -} - -export class ExerciseEntry extends HistoryEntry { - readonly exerciseId: number; - - constructor(exerciseId: number) { - super(); - this.exerciseId = exerciseId; - } -} diff --git a/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.html b/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.html deleted file mode 100644 index 8002222110b2..000000000000 --- a/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.html +++ /dev/null @@ -1,17 +0,0 @@ -
    -
    -
    - - - - -
    -
    -
    - -
    -
    diff --git a/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.ts b/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.ts deleted file mode 100644 index 5ad133a7b589..000000000000 --- a/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Component, Input } from '@angular/core'; -import { HttpErrorResponse } from '@angular/common/http'; -import { LectureUnit, LectureUnitType } from 'app/entities/lecture-unit/lectureUnit.model'; -import { onError } from 'app/shared/util/global.utils'; -import { Lecture } from 'app/entities/lecture.model'; -import { LectureUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/lectureUnit.service'; -import { AlertService } from 'app/core/util/alert.service'; -import { isCommunicationEnabled, isMessagingEnabled } from 'app/entities/course.model'; -import { DiscussionSectionComponent } from 'app/overview/discussion-section/discussion-section.component'; - -export interface LectureUnitCompletionEvent { - lectureUnit: LectureUnit; - completed: boolean; -} - -@Component({ - selector: 'jhi-learning-path-lecture-unit-view', - templateUrl: './learning-path-lecture-unit-view.component.html', -}) -export class LearningPathLectureUnitViewComponent { - @Input() lecture: Lecture; - @Input() lectureUnit: LectureUnit; - readonly LectureUnitType = LectureUnitType; - - discussionComponent?: DiscussionSectionComponent; - - constructor(private lectureUnitService: LectureUnitService, private alertService: AlertService) {} - - completeLectureUnit(event: LectureUnitCompletionEvent): void { - if (this.lecture && event.lectureUnit.visibleToStudents && event.lectureUnit.completed !== event.completed) { - this.lectureUnitService.setCompletion(event.lectureUnit.id!, this.lecture.id!, event.completed).subscribe({ - next: () => { - event.lectureUnit.completed = event.completed; - }, - error: (res: HttpErrorResponse) => onError(this.alertService, res), - }); - } - } - - protected readonly isMessagingEnabled = isMessagingEnabled; - protected readonly isCommunicationEnabled = isCommunicationEnabled; - - /** - * This function gets called if the router outlet gets activated. This is - * used only for the DiscussionComponent - * @param instance The component instance - */ - onChildActivate(instance: DiscussionSectionComponent) { - this.discussionComponent = instance; // save the reference to the component instance - if (this.lecture) { - instance.lecture = this.lecture; - instance.isCommunicationPage = false; - } - } -} diff --git a/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.module.ts b/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.module.ts deleted file mode 100644 index 33dc55941bda..000000000000 --- a/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.module.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; -import { LearningPathLectureUnitViewComponent } from 'app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component'; -import { ArtemisLectureUnitsModule } from 'app/overview/course-lectures/lecture-units.module'; -import { ArtemisSharedModule } from 'app/shared/shared.module'; -import { Authority } from 'app/shared/constants/authority.constants'; -import { UserRouteAccessService } from 'app/core/auth/user-route-access-service'; - -const routes: Routes = [ - { - path: '', - component: LearningPathLectureUnitViewComponent, - data: { - authorities: [Authority.USER], - pageTitle: 'overview.learningPath', - }, - canActivate: [UserRouteAccessService], - children: [ - { - path: '', - pathMatch: 'full', - loadChildren: () => import('app/overview/discussion-section/discussion-section.module').then((m) => m.DiscussionSectionModule), - }, - ], - }, -]; - -@NgModule({ - imports: [ArtemisSharedModule, RouterModule.forChild(routes), ArtemisLectureUnitsModule], - declarations: [LearningPathLectureUnitViewComponent], - exports: [LearningPathLectureUnitViewComponent], -}) -export class ArtemisLearningPathLectureUnitViewModule {} diff --git a/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html b/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html index 22d9121ccc06..632188f51f4d 100644 --- a/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html +++ b/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html @@ -42,7 +42,13 @@ Competencies - + Learning Paths diff --git a/src/main/webapp/app/overview/course-overview.component.html b/src/main/webapp/app/overview/course-overview.component.html index b1bbce889f7e..5b34697f6bc8 100644 --- a/src/main/webapp/app/overview/course-overview.component.html +++ b/src/main/webapp/app/overview/course-overview.component.html @@ -51,10 +51,6 @@ Competencies - - - Learning Path -
    + + +
    +
    diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-nav.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-nav.component.ts new file mode 100644 index 000000000000..c050632cc577 --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-nav.component.ts @@ -0,0 +1,19 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { faArrowsRotate, faArrowsToEye, faXmark } from '@fortawesome/free-solid-svg-icons'; +import { LearningPathPageableSearchDTO } from 'app/entities/competency/learning-path.model'; + +@Component({ + selector: 'jhi-learning-path-progress-nav', + templateUrl: './learning-path-progress-nav.component.html', +}) +export class LearningPathProgressNavComponent { + @Input() learningPath: LearningPathPageableSearchDTO; + @Output() onRefresh: EventEmitter = new EventEmitter(); + @Output() onCenterView: EventEmitter = new EventEmitter(); + @Output() onClose: EventEmitter = new EventEmitter(); + + // icons + faXmark = faXmark; + faArrowsToEye = faArrowsToEye; + faArrowsRotate = faArrowsRotate; +} diff --git a/src/main/webapp/app/course/learning-paths/learning-path.service.ts b/src/main/webapp/app/course/learning-paths/learning-path.service.ts index ece1bc6b204a..35bbb8d833ca 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path.service.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path.service.ts @@ -2,6 +2,8 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { HttpClient, HttpResponse } from '@angular/common/http'; import { LearningPathHealthDTO } from 'app/entities/competency/learning-path-health.model'; +import { NgxLearningPathDTO } from 'app/entities/competency/learning-path.model'; +import { map } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class LearningPathService { @@ -20,4 +22,21 @@ export class LearningPathService { getHealthStatusForCourse(courseId: number) { return this.httpClient.get(`${this.resourceURL}/courses/${courseId}/learning-path-health`, { observe: 'response' }); } + + getNgxLearningPath(learningPathId: number): Observable> { + return this.httpClient.get(`${this.resourceURL}/learning-path/${learningPathId}`, { observe: 'response' }).pipe( + map((ngxLearningPathResponse) => { + if (!ngxLearningPathResponse.body!.nodes) { + ngxLearningPathResponse.body!.nodes = []; + } + if (!ngxLearningPathResponse.body!.edges) { + ngxLearningPathResponse.body!.edges = []; + } + if (!ngxLearningPathResponse.body!.clusters) { + ngxLearningPathResponse.body!.clusters = []; + } + return ngxLearningPathResponse; + }), + ); + } } diff --git a/src/main/webapp/app/course/learning-paths/learning-paths.module.ts b/src/main/webapp/app/course/learning-paths/learning-paths.module.ts index 4d1a254a674f..8c37905813e9 100644 --- a/src/main/webapp/app/course/learning-paths/learning-paths.module.ts +++ b/src/main/webapp/app/course/learning-paths/learning-paths.module.ts @@ -3,10 +3,29 @@ import { ArtemisSharedModule } from 'app/shared/shared.module'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { LearningPathManagementComponent } from 'app/course/learning-paths/learning-path-management/learning-path-management.component'; +import { NgxGraphModule } from '@swimlane/ngx-graph'; +import { ArtemisLectureUnitsModule } from 'app/overview/course-lectures/lecture-units.module'; +import { ArtemisCompetenciesModule } from 'app/course/competencies/competency.module'; +import { LearningPathProgressModalComponent } from 'app/course/learning-paths/learning-path-management/learning-path-progress-modal.component'; +import { LearningPathGraphComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph.component'; +import { LearningPathProgressNavComponent } from 'app/course/learning-paths/learning-path-management/learning-path-progress-nav.component'; +import { LearningPathGraphNodeComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph-node.component'; +import { CompetencyNodeDetailsComponent } from 'app/course/learning-paths/learning-path-graph/node-details/competency-node-details.component'; +import { LectureUnitNodeDetailsComponent } from 'app/course/learning-paths/learning-path-graph/node-details/lecture-unit-node-details.component'; +import { ExerciseNodeDetailsComponent } from 'app/course/learning-paths/learning-path-graph/node-details/exercise-node-details.component'; @NgModule({ - imports: [ArtemisSharedModule, FormsModule, ReactiveFormsModule, ArtemisSharedComponentModule], - declarations: [LearningPathManagementComponent], + imports: [ArtemisSharedModule, FormsModule, ReactiveFormsModule, ArtemisSharedComponentModule, NgxGraphModule, ArtemisLectureUnitsModule, ArtemisCompetenciesModule], + declarations: [ + LearningPathManagementComponent, + LearningPathProgressModalComponent, + LearningPathProgressNavComponent, + LearningPathGraphComponent, + LearningPathGraphNodeComponent, + CompetencyNodeDetailsComponent, + LectureUnitNodeDetailsComponent, + ExerciseNodeDetailsComponent, + ], exports: [], }) export class ArtemisLearningPathsModule {} diff --git a/src/test/javascript/spec/component/learning-paths/graph/learning-path-graph-node.component.spec.ts b/src/test/javascript/spec/component/learning-paths/graph/learning-path-graph-node.component.spec.ts new file mode 100644 index 000000000000..8371428d66ed --- /dev/null +++ b/src/test/javascript/spec/component/learning-paths/graph/learning-path-graph-node.component.spec.ts @@ -0,0 +1,46 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ArtemisTestModule } from '../../../test.module'; +import { MockDirective } from 'ng-mocks'; +import { By } from '@angular/platform-browser'; +import { LearningPathGraphNodeComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph-node.component'; +import { NgxLearningPathNode, NodeType } from 'app/entities/competency/learning-path.model'; +import { StickyPopoverDirective } from 'app/shared/sticky-popover/sticky-popover.directive'; + +describe('LearningPathGraphNodeComponent', () => { + let fixture: ComponentFixture; + let comp: LearningPathGraphNodeComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule], + declarations: [LearningPathGraphNodeComponent, MockDirective(StickyPopoverDirective)], + providers: [], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(LearningPathGraphNodeComponent); + comp = fixture.componentInstance; + }); + }); + + it.each([NodeType.EXERCISE, NodeType.LECTURE_UNIT])('should display correct icon for completed learning object', (type: NodeType) => { + comp.node = { id: '1', type: type, completed: true } as NgxLearningPathNode; + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('#completed')).nativeElement).toBeTruthy(); + }); + + it.each([NodeType.EXERCISE, NodeType.LECTURE_UNIT])('should display correct icon for not completed learning object', (type: NodeType) => { + comp.node = { id: '1', type: type, completed: false } as NgxLearningPathNode; + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('#not-completed')).nativeElement).toBeTruthy(); + }); + + it.each([NodeType.COMPETENCY_START, NodeType.COMPETENCY_END, NodeType.COMPETENCY_START, NodeType.COMPETENCY_END])( + 'should display correct icon for generic node', + (type: NodeType) => { + comp.node = { id: '1', type: type } as NgxLearningPathNode; + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('#generic')).nativeElement).toBeTruthy(); + }, + ); +}); diff --git a/src/test/javascript/spec/component/learning-paths/graph/learning-path-graph.component.spec.ts b/src/test/javascript/spec/component/learning-paths/graph/learning-path-graph.component.spec.ts new file mode 100644 index 000000000000..32dcaacea7b2 --- /dev/null +++ b/src/test/javascript/spec/component/learning-paths/graph/learning-path-graph.component.spec.ts @@ -0,0 +1,62 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ArtemisTestModule } from '../../../test.module'; +import { LearningPathGraphComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph.component'; +import { LearningPathService } from 'app/course/learning-paths/learning-path.service'; + +describe('LearningPathGraphComponent', () => { + let fixture: ComponentFixture; + let comp: LearningPathGraphComponent; + let learningPathService: LearningPathService; + let getNgxLearningPathStub: jest.SpyInstance; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule], + declarations: [LearningPathGraphComponent], + providers: [], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(LearningPathGraphComponent); + comp = fixture.componentInstance; + learningPathService = TestBed.inject(LearningPathService); + getNgxLearningPathStub = jest.spyOn(learningPathService, 'getNgxLearningPath'); + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should load learning path from service', () => { + comp.learningPathId = 1; + fixture.detectChanges(); + expect(getNgxLearningPathStub).toHaveBeenCalledOnce(); + expect(getNgxLearningPathStub).toHaveBeenCalledWith(1); + }); + + it('should update, center, and zoom to fit on resize', () => { + const updateStub = jest.spyOn(comp.update$, 'next'); + const centerStub = jest.spyOn(comp.center$, 'next'); + const zoomToFitStub = jest.spyOn(comp.zoomToFit$, 'next'); + fixture.detectChanges(); + comp.onResize(); + expect(updateStub).toHaveBeenCalledOnce(); + expect(updateStub).toHaveBeenCalledWith(true); + expect(centerStub).toHaveBeenCalledOnce(); + expect(centerStub).toHaveBeenCalledWith(true); + expect(zoomToFitStub).toHaveBeenCalledOnce(); + expect(zoomToFitStub).toHaveBeenCalledWith(true); + }); + + it('should zoom to fit and center on resize', () => { + const zoomToFitStub = jest.spyOn(comp.zoomToFit$, 'next'); + const centerStub = jest.spyOn(comp.center$, 'next'); + fixture.detectChanges(); + comp.onCenterView(); + expect(zoomToFitStub).toHaveBeenCalledOnce(); + expect(zoomToFitStub).toHaveBeenCalledWith(true); + expect(centerStub).toHaveBeenCalledOnce(); + expect(centerStub).toHaveBeenCalledWith(true); + }); +}); diff --git a/src/test/javascript/spec/component/learning-paths/graph/node-details/competency-node-details.component.spec.ts b/src/test/javascript/spec/component/learning-paths/graph/node-details/competency-node-details.component.spec.ts new file mode 100644 index 000000000000..848c9a2cefe9 --- /dev/null +++ b/src/test/javascript/spec/component/learning-paths/graph/node-details/competency-node-details.component.spec.ts @@ -0,0 +1,69 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ArtemisTestModule } from '../../../../test.module'; +import { MockComponent, MockPipe } from 'ng-mocks'; +import { of } from 'rxjs'; +import { HttpResponse } from '@angular/common/http'; +import { CompetencyNodeDetailsComponent } from 'app/course/learning-paths/learning-path-graph/node-details/competency-node-details.component'; +import { Competency, CompetencyProgress, CompetencyTaxonomy } from 'app/entities/competency.model'; +import { CompetencyService } from 'app/course/competencies/competency.service'; +import { CompetencyRingsComponent } from 'app/course/competencies/competency-rings/competency-rings.component'; +import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { NgbTooltipMocksModule } from '../../../../helpers/mocks/directive/ngbTooltipMocks.module'; + +describe('CompetencyNodeDetailsComponent', () => { + let fixture: ComponentFixture; + let comp: CompetencyNodeDetailsComponent; + let competencyService: CompetencyService; + let findByIdStub: jest.SpyInstance; + let competency: Competency; + let competencyProgress: CompetencyProgress; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule, NgbTooltipMocksModule], + declarations: [CompetencyNodeDetailsComponent, MockComponent(CompetencyRingsComponent), MockPipe(ArtemisTranslatePipe)], + providers: [], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(CompetencyNodeDetailsComponent); + comp = fixture.componentInstance; + competency = new Competency(); + competency.id = 2; + competency.title = 'Some arbitrary title'; + competency.description = 'Some description'; + competency.taxonomy = CompetencyTaxonomy.ANALYZE; + competency.masteryThreshold = 50; + competencyProgress = new CompetencyProgress(); + competencyProgress.progress = 80; + competencyProgress.confidence = 70; + competency.userProgress = [competencyProgress]; + + competencyService = TestBed.inject(CompetencyService); + findByIdStub = jest.spyOn(competencyService, 'findById').mockReturnValue(of(new HttpResponse({ body: competency }))); + comp.courseId = 1; + comp.competencyId = competency.id; + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should load competency on init', () => { + fixture.detectChanges(); + expect(findByIdStub).toHaveBeenCalledOnce(); + expect(findByIdStub).toHaveBeenCalledWith(competency.id, 1); + expect(comp.competency).toEqual(competency); + expect(comp.competencyProgress).toEqual(competencyProgress); + }); + + it('should default progress to zero if empty', () => { + competency.userProgress = undefined; + fixture.detectChanges(); + expect(findByIdStub).toHaveBeenCalledOnce(); + expect(findByIdStub).toHaveBeenCalledWith(competency.id, 1); + expect(comp.competency).toEqual(competency); + expect(comp.competencyProgress).toEqual({ confidence: 0, progress: 0 } as CompetencyProgress); + }); +}); diff --git a/src/test/javascript/spec/component/learning-paths/graph/node-details/exercise-node-details.component.spec.ts b/src/test/javascript/spec/component/learning-paths/graph/node-details/exercise-node-details.component.spec.ts new file mode 100644 index 000000000000..839aeccf50de --- /dev/null +++ b/src/test/javascript/spec/component/learning-paths/graph/node-details/exercise-node-details.component.spec.ts @@ -0,0 +1,50 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ArtemisTestModule } from '../../../../test.module'; +import { MockPipe } from 'ng-mocks'; +import { of } from 'rxjs'; +import { HttpResponse } from '@angular/common/http'; +import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { NgbTooltipMocksModule } from '../../../../helpers/mocks/directive/ngbTooltipMocks.module'; +import { ExerciseNodeDetailsComponent } from 'app/course/learning-paths/learning-path-graph/node-details/exercise-node-details.component'; +import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; +import { Exercise } from 'app/entities/exercise.model'; +import { TextExercise } from 'app/entities/text-exercise.model'; + +describe('ExerciseNodeDetailsComponent', () => { + let fixture: ComponentFixture; + let comp: ExerciseNodeDetailsComponent; + let exerciseService: ExerciseService; + let findStub: jest.SpyInstance; + let exercise: Exercise; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule, NgbTooltipMocksModule], + declarations: [ExerciseNodeDetailsComponent, MockPipe(ArtemisTranslatePipe)], + providers: [], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(ExerciseNodeDetailsComponent); + comp = fixture.componentInstance; + exercise = new TextExercise(undefined, undefined); + exercise.id = 1; + exercise.title = 'Some arbitrary title'; + + exerciseService = TestBed.inject(ExerciseService); + findStub = jest.spyOn(exerciseService, 'find').mockReturnValue(of(new HttpResponse({ body: exercise }))); + comp.exerciseId = exercise.id; + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should load exercise on init', () => { + fixture.detectChanges(); + expect(findStub).toHaveBeenCalledOnce(); + expect(findStub).toHaveBeenCalledWith(exercise.id); + expect(comp.exercise).toEqual(exercise); + }); +}); diff --git a/src/test/javascript/spec/component/learning-paths/graph/node-details/lecture-unit-node-details.component.spec.ts b/src/test/javascript/spec/component/learning-paths/graph/node-details/lecture-unit-node-details.component.spec.ts new file mode 100644 index 000000000000..0c7a44364c97 --- /dev/null +++ b/src/test/javascript/spec/component/learning-paths/graph/node-details/lecture-unit-node-details.component.spec.ts @@ -0,0 +1,57 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ArtemisTestModule } from '../../../../test.module'; +import { MockPipe } from 'ng-mocks'; +import { of } from 'rxjs'; +import { HttpResponse } from '@angular/common/http'; +import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { NgbTooltipMocksModule } from '../../../../helpers/mocks/directive/ngbTooltipMocks.module'; +import { LectureUnitNodeDetailsComponent } from 'app/course/learning-paths/learning-path-graph/node-details/lecture-unit-node-details.component'; +import { LectureService } from 'app/lecture/lecture.service'; +import { Lecture } from 'app/entities/lecture.model'; +import { LectureUnit } from 'app/entities/lecture-unit/lectureUnit.model'; +import { TextUnit } from 'app/entities/lecture-unit/textUnit.model'; + +describe('LectureUnitNodeDetailsComponent', () => { + let fixture: ComponentFixture; + let comp: LectureUnitNodeDetailsComponent; + let lectureService: LectureService; + let findWithDetailsStub: jest.SpyInstance; + let lecture: Lecture; + let lectureUnit: LectureUnit; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule, NgbTooltipMocksModule], + declarations: [LectureUnitNodeDetailsComponent, MockPipe(ArtemisTranslatePipe)], + providers: [], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(LectureUnitNodeDetailsComponent); + comp = fixture.componentInstance; + lecture = new Lecture(); + lecture.id = 1; + lectureUnit = new TextUnit(); + lectureUnit.id = 2; + lectureUnit.name = 'Some arbitrary name'; + lecture.lectureUnits = [lectureUnit]; + + lectureService = TestBed.inject(LectureService); + findWithDetailsStub = jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(of(new HttpResponse({ body: lecture }))); + comp.lectureId = lecture.id; + comp.lectureUnitId = lectureUnit.id; + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should load lecture unit on init', () => { + fixture.detectChanges(); + expect(findWithDetailsStub).toHaveBeenCalledOnce(); + expect(findWithDetailsStub).toHaveBeenCalledWith(lecture.id); + expect(comp.lecture).toEqual(lecture); + expect(comp.lectureUnit).toEqual(lectureUnit); + }); +}); diff --git a/src/test/javascript/spec/component/learning-paths/management/learning-path-progress-modal.component.spec.ts b/src/test/javascript/spec/component/learning-paths/management/learning-path-progress-modal.component.spec.ts new file mode 100644 index 000000000000..1925146cac25 --- /dev/null +++ b/src/test/javascript/spec/component/learning-paths/management/learning-path-progress-modal.component.spec.ts @@ -0,0 +1,48 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ArtemisTestModule } from '../../../test.module'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { MockComponent } from 'ng-mocks'; +import { LearningPathProgressModalComponent } from 'app/course/learning-paths/learning-path-management/learning-path-progress-modal.component'; +import { LearningPathGraphComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph.component'; +import { By } from '@angular/platform-browser'; +import { LearningPathProgressNavComponent } from 'app/course/learning-paths/learning-path-management/learning-path-progress-nav.component'; +import { LearningPathPageableSearchDTO } from 'app/entities/competency/learning-path.model'; + +describe('LearningPathProgressModalComponent', () => { + let fixture: ComponentFixture; + let comp: LearningPathProgressModalComponent; + let activeModal: NgbActiveModal; + let closeStub: jest.SpyInstance; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule, MockComponent(LearningPathGraphComponent), MockComponent(LearningPathProgressNavComponent)], + declarations: [LearningPathProgressModalComponent], + providers: [], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(LearningPathProgressModalComponent); + comp = fixture.componentInstance; + activeModal = TestBed.inject(NgbActiveModal); + closeStub = jest.spyOn(activeModal, 'close'); + fixture.detectChanges(); + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should display learning path graph if learning path is present', () => { + comp.courseId = 2; + comp.learningPath = { id: 1 } as LearningPathPageableSearchDTO; + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('.graph')).nativeElement).toBeTruthy(); + }); + + it('should correctly close modal', () => { + comp.close(); + expect(closeStub).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/test/javascript/spec/component/learning-paths/management/learning-path-progress-nav.component.spec.ts b/src/test/javascript/spec/component/learning-paths/management/learning-path-progress-nav.component.spec.ts new file mode 100644 index 000000000000..437b2781c3ea --- /dev/null +++ b/src/test/javascript/spec/component/learning-paths/management/learning-path-progress-nav.component.spec.ts @@ -0,0 +1,73 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ArtemisTestModule } from '../../../test.module'; +import { By } from '@angular/platform-browser'; +import { LearningPathProgressNavComponent } from 'app/course/learning-paths/learning-path-management/learning-path-progress-nav.component'; +import { LearningPathPageableSearchDTO } from 'app/entities/competency/learning-path.model'; +import { UserNameAndLoginDTO } from 'app/core/user/user.model'; +import { MockPipe } from 'ng-mocks'; +import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { NgbTooltipMocksModule } from '../../../helpers/mocks/directive/ngbTooltipMocks.module'; + +describe('LearningPathProgressNavComponent', () => { + let fixture: ComponentFixture; + let comp: LearningPathProgressNavComponent; + let onRefreshStub: jest.SpyInstance; + let onCenterViewStub: jest.SpyInstance; + let onCloseStub: jest.SpyInstance; + let learningPath: LearningPathPageableSearchDTO; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule, NgbTooltipMocksModule], + declarations: [LearningPathProgressNavComponent, MockPipe(ArtemisTranslatePipe)], + providers: [], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(LearningPathProgressNavComponent); + comp = fixture.componentInstance; + onRefreshStub = jest.spyOn(comp.onRefresh, 'emit'); + onCenterViewStub = jest.spyOn(comp.onCenterView, 'emit'); + onCloseStub = jest.spyOn(comp.onClose, 'emit'); + learningPath = new LearningPathPageableSearchDTO(); + learningPath.user = new UserNameAndLoginDTO(); + learningPath.user.name = 'some arbitrary name'; + learningPath.user.login = 'somearbitrarylogin'; + comp.learningPath = learningPath; + fixture.detectChanges(); + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should create', () => { + expect(fixture).toBeTruthy(); + expect(comp).toBeTruthy(); + }); + + it('should emit refresh on click', () => { + const button = fixture.debugElement.query(By.css('#refresh-button')); + expect(button).not.toBeNull(); + + button.nativeElement.click(); + expect(onRefreshStub).toHaveBeenCalledOnce(); + }); + + it('should emit center view on click', () => { + const button = fixture.debugElement.query(By.css('#center-button')); + expect(button).not.toBeNull(); + + button.nativeElement.click(); + expect(onCenterViewStub).toHaveBeenCalledOnce(); + }); + + it('should emit close on click', () => { + const button = fixture.debugElement.query(By.css('#close-button')); + expect(button).not.toBeNull(); + + button.nativeElement.click(); + expect(onCloseStub).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/test/javascript/spec/service/learning-path.service.spec.ts b/src/test/javascript/spec/service/learning-path.service.spec.ts index cbd2601a8266..0a85e60ff614 100644 --- a/src/test/javascript/spec/service/learning-path.service.spec.ts +++ b/src/test/javascript/spec/service/learning-path.service.spec.ts @@ -45,4 +45,10 @@ describe('LearningPathService', () => { expect(getStub).toHaveBeenCalledOnce(); expect(getStub).toHaveBeenCalledWith('api/courses/1/learning-path-health', { observe: 'response' }); }); + + it('should send a request to the server to get ngx representation of learning path', () => { + learningPathService.getNgxLearningPath(1).subscribe(); + expect(getStub).toHaveBeenCalledOnce(); + expect(getStub).toHaveBeenCalledWith('api/learning-path/1', { observe: 'response' }); + }); }); From 7be6cc0fbe02e96702de283fca71b97038e09c3e Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Wed, 2 Aug 2023 15:39:50 +0200 Subject: [PATCH 124/215] add client DTOs --- .../competency/learning-path.model.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/main/webapp/app/entities/competency/learning-path.model.ts b/src/main/webapp/app/entities/competency/learning-path.model.ts index aee01ef000cd..8c2978e39963 100644 --- a/src/main/webapp/app/entities/competency/learning-path.model.ts +++ b/src/main/webapp/app/entities/competency/learning-path.model.ts @@ -2,6 +2,7 @@ import { BaseEntity } from 'app/shared/model/base-entity'; import { Course } from 'app/entities/course.model'; import { User, UserNameAndLoginDTO } from 'app/core/user/user.model'; import { Competency } from 'app/entities/competency.model'; +import { ClusterNode, Edge, Node } from '@swimlane/ngx-graph'; export class LearningPath implements BaseEntity { public id?: number; @@ -18,3 +19,38 @@ export class LearningPathPageableSearchDTO { public user?: UserNameAndLoginDTO; public progress?: number; } + +export class NgxLearningPathDTO { + public nodes: NgxLearningPathNode[]; + public edges: NgxLearningPathEdge[]; + public clusters: NgxLearningPathCluster[]; +} + +export class NgxLearningPathNode implements Node { + public id: string; + public type?: NodeType; + public linkedResource?: number; + public linkedResourceParent?: number; + public completed?: boolean; + public label?: string; +} + +export class NgxLearningPathEdge implements Edge { + public id?: string; + public source: string; + public target: string; +} + +export class NgxLearningPathCluster implements ClusterNode { + public id: string; + public label?: string; + public childNodeIds?: string[]; +} +export enum NodeType { + COMPETENCY_START = 'COMPETENCY_START', + COMPETENCY_END = 'COMPETENCY_END', + MATCH_START = 'MATCH_START', + MATCH_END = 'MATCH_END', + EXERCISE = 'EXERCISE', + LECTURE_UNIT = 'LECTURE_UNIT', +} From ea276949540da320e4f74e89e33f56b4f883981a Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Wed, 2 Aug 2023 15:40:15 +0200 Subject: [PATCH 125/215] add modal to learning path management --- .../learning-path-management.component.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts index 96441d25d553..f8cafc4a67f0 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts @@ -12,6 +12,8 @@ import { SortService } from 'app/shared/service/sort.service'; import { LearningPathPageableSearchDTO } from 'app/entities/competency/learning-path.model'; import { faSort, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons'; import { HealthStatus, LearningPathHealthDTO } from 'app/entities/competency/learning-path-health.model'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { LearningPathProgressModalComponent } from 'app/course/learning-paths/learning-path-management/learning-path-progress-modal.component'; export enum TableColumn { ID = 'ID', @@ -55,6 +57,7 @@ export class LearningPathManagementComponent implements OnInit { private alertService: AlertService, private pagingService: LearningPathPagingService, private sortService: SortService, + private modalService: NgbModal, ) {} get page(): number { @@ -200,7 +203,13 @@ export class LearningPathManagementComponent implements OnInit { } } viewLearningPath(learningPath: LearningPathPageableSearchDTO) { - // todo: part of future pr + const modalRef = this.modalService.open(LearningPathProgressModalComponent, { + size: 'xl', + backdrop: 'static', + windowClass: 'learning-path-modal', + }); + modalRef.componentInstance.courseId = this.courseId; + modalRef.componentInstance.learningPath = learningPath; } protected readonly HealthStatus = HealthStatus; From cd36231a5ab70a43fe41f1155436a7eb7541a3f8 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Wed, 2 Aug 2023 16:02:25 +0200 Subject: [PATCH 126/215] add server code and tests --- build.gradle | 1 + .../repository/LearningPathRepository.java | 15 ++ .../artemis/service/LearningPathService.java | 240 +++++++++++++++++- .../web/rest/LearningPathResource.java | 30 +++ .../dto/competency/NgxLearningPathDTO.java | 47 ++++ .../lecture/LearningPathIntegrationTest.java | 50 ++++ .../service/LearningPathServiceTest.java | 223 ++++++++++++++++ 7 files changed, 605 insertions(+), 1 deletion(-) create mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/NgxLearningPathDTO.java diff --git a/build.gradle b/build.gradle index cd60acf199f7..8c0f0c832233 100644 --- a/build.gradle +++ b/build.gradle @@ -340,6 +340,7 @@ dependencies { implementation "de.tum.in.ase.athene:client:0.0.2" implementation "commons-fileupload:commons-fileupload:1.5" implementation "net.lingala.zip4j:zip4j:2.11.5" + implementation "org.jgrapht:jgrapht-core:1.5.2" annotationProcessor "org.hibernate:hibernate-jpamodelgen:${hibernate_version}" diff --git a/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java index f48c5ee6b36c..5f5745217413 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/LearningPathRepository.java @@ -4,6 +4,8 @@ import java.util.Optional; +import javax.validation.constraints.NotNull; + import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.EntityGraph; @@ -20,6 +22,10 @@ public interface LearningPathRepository extends JpaRepository findByCourseIdAndUserId(long courseId, long userId); + default LearningPath findByCourseIdAndUserIdElseThrow(long courseId, long userId) { + return findByCourseIdAndUserId(courseId, userId).orElseThrow(() -> new EntityNotFoundException("LearningPath")); + } + @EntityGraph(type = LOAD, attributePaths = { "competencies" }) Optional findWithEagerCompetenciesByCourseIdAndUserId(long courseId, long userId); @@ -40,4 +46,13 @@ SELECT COUNT (learningPath) WHERE learningPath.course.id = :courseId AND learningPath.user.isDeleted = false AND learningPath.course.studentGroupName MEMBER OF learningPath.user.groups """) long countLearningPathsOfEnrolledStudentsInCourse(@Param("courseId") long courseId); + + @EntityGraph(type = LOAD, attributePaths = { "competencies", "competencies.lectureUnits", "competencies.lectureUnits.completedUsers", "competencies.exercises", + "competencies.exercises.studentParticipations" }) + Optional findWithEagerCompetenciesAndLearningObjectsAndCompletedUsersById(long learningPathId); + + @NotNull + default LearningPath findWithEagerCompetenciesAndLearningObjectsAndCompletedUsersByIdElseThrow(long learningPathId) { + return findWithEagerCompetenciesAndLearningObjectsAndCompletedUsersById(learningPathId).orElseThrow(() -> new EntityNotFoundException("LearningPath", learningPathId)); + } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index dba822bdd7fa..3917041d9798 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -1,10 +1,14 @@ package de.tum.in.www1.artemis.service; import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; +import java.util.stream.LongStream; +import java.util.stream.Stream; import javax.validation.constraints.NotNull; +import org.jgrapht.alg.util.UnionFind; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; @@ -13,12 +17,14 @@ import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.competency.Competency; +import de.tum.in.www1.artemis.domain.competency.CompetencyRelation; import de.tum.in.www1.artemis.domain.competency.LearningPath; import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.web.rest.dto.PageableSearchDTO; import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; import de.tum.in.www1.artemis.web.rest.dto.competency.LearningPathHealthDTO; import de.tum.in.www1.artemis.web.rest.dto.competency.LearningPathPageableSearchDTO; +import de.tum.in.www1.artemis.web.rest.dto.competency.NgxLearningPathDTO; import de.tum.in.www1.artemis.web.rest.util.PageUtil; /** @@ -45,12 +51,15 @@ public class LearningPathService { private final CourseRepository courseRepository; + private final CompetencyRelationRepository competencyRelationRepository; + public LearningPathService(UserRepository userRepository, LearningPathRepository learningPathRepository, CompetencyProgressRepository competencyProgressRepository, - CourseRepository courseRepository) { + CourseRepository courseRepository, CompetencyRelationRepository competencyRelationRepository) { this.userRepository = userRepository; this.learningPathRepository = learningPathRepository; this.competencyProgressRepository = competencyProgressRepository; this.courseRepository = courseRepository; + this.competencyRelationRepository = competencyRelationRepository; } /** @@ -184,4 +193,233 @@ public LearningPathHealthDTO getHealthStatusForCourse(Course course) { return new LearningPathHealthDTO(LearningPathHealthDTO.HealthStatus.MISSING, numberOfStudents - numberOfLearningPaths); } } + + /** + * Generates Ngx representation of the learning path. + * + * @param learningPath the learning path for which the Ngx representation should be created + * @return Ngx representation of the learning path + * @see NgxLearningPathDTO + */ + public NgxLearningPathDTO generateNgxRepresentation(@NotNull LearningPath learningPath) { + Set nodes = new HashSet<>(); + Set edges = new HashSet<>(); + Set clusters = new HashSet<>(); + learningPath.getCompetencies().forEach(competency -> generateNgxRepresentationForCompetency(learningPath, competency, nodes, edges, clusters)); + generateNgxRepresentationForRelations(learningPath, nodes, edges); + return new NgxLearningPathDTO(nodes, edges, clusters); + } + + /** + * Generates Ngx representation for competency. + *

    + * A competency's representation consists of + *

      + *
    • start node
    • + *
    • end node
    • + *
    • a node for each learning unit (exercises or lecture unit)
    • + *
    • edges from start node to each learning unit
    • + *
    • edges from each learning unit to end node
    • + *
    • a cluster consisting of all created nodes
    • + *
    + * + * @param learningPath the learning path for which the representation should be created + * @param competency the competency for which the representation will be created + * @param nodes set of nodes to store the new nodes + * @param edges set of edges to store the new edges + * @param clusters set of clusters to store the new clusters + */ + private void generateNgxRepresentationForCompetency(LearningPath learningPath, Competency competency, Set nodes, Set edges, + Set clusters) { + Set currentCluster = new HashSet<>(); + // generates start and end node + final var startNodeId = getCompetencyStartNodeId(competency.getId()); + final var endNodeId = getCompetencyEndNodeId(competency.getId()); + currentCluster.add(new NgxLearningPathDTO.Node(startNodeId, NgxLearningPathDTO.NodeType.COMPETENCY_START, competency.getId())); + currentCluster.add(new NgxLearningPathDTO.Node(endNodeId, NgxLearningPathDTO.NodeType.COMPETENCY_END, competency.getId())); + + // generate nodes and edges for lecture units + competency.getLectureUnits().forEach(lectureUnit -> { + currentCluster.add(new NgxLearningPathDTO.Node(getLectureUnitNodeId(competency.getId(), lectureUnit.getId()), NgxLearningPathDTO.NodeType.LECTURE_UNIT, + lectureUnit.getId(), lectureUnit.getLecture().getId(), lectureUnit.isCompletedFor(learningPath.getUser()), lectureUnit.getName())); + edges.add(new NgxLearningPathDTO.Edge(getLectureUnitInEdgeId(competency.getId(), lectureUnit.getId()), startNodeId, + getLectureUnitNodeId(competency.getId(), lectureUnit.getId()))); + edges.add(new NgxLearningPathDTO.Edge(getLectureUnitOutEdgeId(competency.getId(), lectureUnit.getId()), getLectureUnitNodeId(competency.getId(), lectureUnit.getId()), + endNodeId)); + }); + // generate nodes and edges for exercises + competency.getExercises().forEach(exercise -> { + currentCluster.add(new NgxLearningPathDTO.Node(getExerciseNodeId(competency.getId(), exercise.getId()), NgxLearningPathDTO.NodeType.EXERCISE, exercise.getId(), + exercise.isCompletedFor(learningPath.getUser()), exercise.getTitle())); + edges.add(new NgxLearningPathDTO.Edge(getExerciseInEdgeId(competency.getId(), exercise.getId()), startNodeId, getExerciseNodeId(competency.getId(), exercise.getId()))); + edges.add(new NgxLearningPathDTO.Edge(getExerciseOutEdgeId(competency.getId(), exercise.getId()), getExerciseNodeId(competency.getId(), exercise.getId()), endNodeId)); + }); + // if no linked learning units exist directly link start to end + if (currentCluster.size() == 2) { + edges.add(new NgxLearningPathDTO.Edge(getDirectEdgeId(competency.getId()), startNodeId, endNodeId)); + } + // generate cluster for competency + var childNodeIds = currentCluster.stream().map(NgxLearningPathDTO.Node::id).collect(Collectors.toSet()); + childNodeIds.add(startNodeId); + childNodeIds.add(endNodeId); + clusters.add(new NgxLearningPathDTO.Cluster(String.valueOf(competency.getId()), competency.getTitle(), childNodeIds)); + + nodes.addAll(currentCluster); + } + + /** + * Generates Ngx representations for competency relations. + *

    + * The representation will contain: + *

      + *
    • + * For each matching cluster (transitive closure of competencies that are in a match relation): + *
        + *
      • two nodes (start and end of cluster) will be created
      • + *
      • edges from the start node of the cluster to each start node of the competencies
      • + *
      • edges from each end node of the competency to the end node of the cluster
      • + *
      + *
    • + *
    • + * For each other relation: edge from head competency end node to tail competency start node. If competency is part of a matching cluster, the edge will be linked to the + * corresponding cluster start/end node. + *
    • + *
    + * + * two nodes (start and end of cluster) will be created. + * + * @param learningPath the learning path for which the Ngx representation should be created + * @param nodes set of nodes to store the new nodes + * @param edges set of edges to store the new edges + */ + private void generateNgxRepresentationForRelations(LearningPath learningPath, Set nodes, Set edges) { + final var relations = competencyRelationRepository.findAllByCourseId(learningPath.getCourse().getId()); + + // compute match clusters + Map competencyToMatchCluster = new HashMap<>(); + final var competenciesInMatchRelation = relations.stream().filter(relation -> relation.getType().equals(CompetencyRelation.RelationType.MATCHES)) + .flatMap(relation -> Stream.of(relation.getHeadCompetency().getId(), relation.getTailCompetency().getId())).collect(Collectors.toSet()); + if (!competenciesInMatchRelation.isEmpty()) { + UnionFind matchClusters = new UnionFind<>(competenciesInMatchRelation); + relations.stream().filter(relation -> relation.getType().equals(CompetencyRelation.RelationType.MATCHES)) + .forEach(relation -> matchClusters.union(relation.getHeadCompetency().getId(), relation.getTailCompetency().getId())); + + // generate map between competencies and cluster node + AtomicInteger matchClusterId = new AtomicInteger(); + relations.stream().filter(relation -> relation.getType().equals(CompetencyRelation.RelationType.MATCHES)) + .flatMapToLong(relation -> LongStream.of(relation.getHeadCompetency().getId(), relation.getTailCompetency().getId())).distinct().forEach(competencyId -> { + var parentId = matchClusters.find(competencyId); + var clusterId = competencyToMatchCluster.computeIfAbsent(parentId, (key) -> matchClusterId.getAndIncrement()); + competencyToMatchCluster.put(competencyId, clusterId); + }); + + // generate match cluster start and end nodes + for (int i = 0; i < matchClusters.numberOfSets(); i++) { + nodes.add(new NgxLearningPathDTO.Node(getMatchingClusterStartNodeId(i), NgxLearningPathDTO.NodeType.COMPETENCY_START)); + nodes.add(new NgxLearningPathDTO.Node(getMatchingClusterEndNodeId(i), NgxLearningPathDTO.NodeType.COMPETENCY_END)); + } + + // generate edges between match cluster nodes and corresponding competencies + competencyToMatchCluster.forEach((competency, cluster) -> { + edges.add(new NgxLearningPathDTO.Edge(getInEdgeId(competency), getMatchingClusterStartNodeId(cluster), getCompetencyStartNodeId(competency))); + edges.add(new NgxLearningPathDTO.Edge(getOutEdgeId(competency), getCompetencyEndNodeId(competency), getMatchingClusterEndNodeId(cluster))); + }); + } + + // generate edges for remaining relations + final Set createdRelations = new HashSet<>(); + relations.stream().filter(relation -> !relation.getType().equals(CompetencyRelation.RelationType.MATCHES)) + .forEach(relation -> generateNgxRepresentationForRelation(relation, competencyToMatchCluster, createdRelations, edges)); + } + + /** + * Generates Ngx representations for competency relation. + * + * @param relation the relation for which the Ngx representation should be created + * @param competencyToMatchCluster map from competencies to corresponding cluster + * @param createdRelations set of edge ids that have already been created + * @param edges set of edges to store the new edges + */ + private void generateNgxRepresentationForRelation(CompetencyRelation relation, Map competencyToMatchCluster, Set createdRelations, + Set edges) { + final var sourceId = relation.getHeadCompetency().getId(); + String sourceNodeId; + if (competencyToMatchCluster.containsKey(sourceId)) { + sourceNodeId = getMatchingClusterEndNodeId(competencyToMatchCluster.get(sourceId)); + } + else { + sourceNodeId = getCompetencyEndNodeId(sourceId); + } + final var targetId = relation.getTailCompetency().getId(); + String targetNodeId; + if (competencyToMatchCluster.containsKey(targetId)) { + targetNodeId = getMatchingClusterStartNodeId(competencyToMatchCluster.get(targetId)); + } + else { + targetNodeId = getCompetencyStartNodeId(targetId); + } + final String relationEdgeId = getRelationEdgeId(sourceNodeId, targetNodeId); + // skip if relation has already been created (possible for edges linked to matching cluster start/end nodes) + if (!createdRelations.contains(relationEdgeId)) { + final var edge = new NgxLearningPathDTO.Edge(relationEdgeId, sourceNodeId, targetNodeId); + edges.add(edge); + createdRelations.add(relationEdgeId); + } + } + + public static String getCompetencyStartNodeId(long competencyId) { + return "node-" + competencyId + "-start"; + } + + public static String getCompetencyEndNodeId(long competencyId) { + return "node-" + competencyId + "-end"; + } + + public static String getLectureUnitNodeId(long competencyId, long lectureUnitId) { + return "node-" + competencyId + "-lu-" + lectureUnitId; + } + + public static String getExerciseNodeId(long competencyId, long exerciseId) { + return "node-" + competencyId + "-ex-" + exerciseId; + } + + public static String getMatchingClusterStartNodeId(long matchingClusterId) { + return "matching-" + matchingClusterId + "-start"; + } + + public static String getMatchingClusterEndNodeId(long matchingClusterId) { + return "matching-" + matchingClusterId + "-end"; + } + + public static String getLectureUnitInEdgeId(long competencyId, long lectureUnitId) { + return "edge-" + competencyId + "-lu-" + getInEdgeId(lectureUnitId); + } + + public static String getLectureUnitOutEdgeId(long competencyId, long lectureUnitId) { + return "edge-" + competencyId + "-lu-" + getOutEdgeId(lectureUnitId); + } + + public static String getExerciseInEdgeId(long competencyId, long exercise) { + return "edge-" + competencyId + "-ex-" + getInEdgeId(exercise); + } + + public static String getExerciseOutEdgeId(long competencyId, long exercise) { + return "edge-" + competencyId + "-ex-" + getOutEdgeId(exercise); + } + + public static String getInEdgeId(long id) { + return "edge-" + id + "-in"; + } + + public static String getOutEdgeId(long id) { + return "edge-" + id + "-out"; + } + + public static String getRelationEdgeId(String sourceNodeId, String targetNodeId) { + return "edge-relation-" + sourceNodeId + "-" + targetNodeId; + } + + public static String getDirectEdgeId(long competencyId) { + return "edge-" + competencyId + "-direct"; + } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java index fca6892e86f5..e4c2e458a1b7 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java @@ -8,11 +8,13 @@ import org.springframework.web.bind.annotation.*; import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.competency.LearningPath; import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.LearningPathRepository; import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.security.Role; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor; +import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent; import de.tum.in.www1.artemis.service.AuthorizationCheckService; import de.tum.in.www1.artemis.service.LearningPathService; import de.tum.in.www1.artemis.web.rest.dto.PageableSearchDTO; @@ -125,4 +127,32 @@ public ResponseEntity getHealthStatusForCourse(@PathVaria return ResponseEntity.ok(learningPathService.getHealthStatusForCourse(course)); } + + /** + * GET /learning-path/:learningPathId : Gets the ngx representation of the learning path. + * + * @param learningPathId the id of the learning path that should be fetched + * @return the ResponseEntity with status 200 (OK) and with body the ngx representation of the learning path + */ + @GetMapping("/learning-path/{learningPathId}") + @EnforceAtLeastStudent + public ResponseEntity getNgxLearningPath(@PathVariable Long learningPathId) { + log.debug("REST request to get ngx representation of learning path with id: {}", learningPathId); + LearningPath learningPath = learningPathRepository.findWithEagerCompetenciesAndLearningObjectsAndCompletedUsersByIdElseThrow(learningPathId); + Course course = courseRepository.findByIdElseThrow(learningPath.getCourse().getId()); + if (!course.getLearningPathsEnabled()) { + throw new BadRequestException("Learning paths are not enabled for this course."); + } + if (authorizationCheckService.isStudentInCourse(course, null)) { + final var user = userRepository.getUser(); + if (!user.getId().equals(learningPath.getUser().getId())) { + throw new AccessForbiddenException("You are not allowed to access another users learning path."); + } + } + else if (!authorizationCheckService.isAtLeastInstructorInCourse(course, null) && !authorizationCheckService.isAdmin()) { + throw new AccessForbiddenException("You are not allowed to access another users learning path."); + } + NgxLearningPathDTO graph = learningPathService.generateNgxRepresentation(learningPath); + return ResponseEntity.ok(graph); + } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/NgxLearningPathDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/NgxLearningPathDTO.java new file mode 100644 index 000000000000..f083b7eb4c52 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/NgxLearningPathDTO.java @@ -0,0 +1,47 @@ +package de.tum.in.www1.artemis.web.rest.dto.competency; + +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Represents simplified learning path optimized for Ngx representation + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record NgxLearningPathDTO(Set nodes, Set edges, Set clusters) { + + public record Node(String id, NodeType type, Long linkedResource, Long linkedResourceParent, boolean completed, String label) { + + public Node(String id, NodeType type, Long linkedResource, boolean completed, String label) { + this(id, type, linkedResource, null, completed, label); + } + + public Node(String id, NodeType type, Long linkedResource, String label) { + this(id, type, linkedResource, false, label); + } + + public Node(String id, NodeType type, String label) { + this(id, type, null, label); + } + + public Node(String id, NodeType type, Long linkedResource) { + this(id, type, linkedResource, ""); + } + + public Node(String id, NodeType type) { + this(id, type, null, ""); + } + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + public record Edge(String id, String source, String target) { + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + public record Cluster(String id, String label, Set childNodeIds) { + } + + public enum NodeType { + COMPETENCY_START, COMPETENCY_END, MATCH_START, MATCH_END, EXERCISE, LECTURE_UNIT, + } +} diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java index 5cb2173e8cc3..a6e9085c3c21 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java @@ -387,4 +387,54 @@ void testUpdateLearningPathProgress() throws Exception { void testGetHealthStatusForCourse() throws Exception { request.get("/api/courses/" + course.getId() + "/learning-path-health", HttpStatus.OK, LearningPathHealthDTO.class); } + + @Test + @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") + void testGetNgxLearningPathForLearningPathsDisabled() throws Exception { + course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); + final var student = userRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); + course.setLearningPathsEnabled(false); + courseRepository.save(course); + request.get("/api/learning-path/" + learningPath.getId(), HttpStatus.BAD_REQUEST, NgxLearningPathDTO.class); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student2", roles = "USER") + void testGetNgxLearningPathForOtherStudent() throws Exception { + course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); + final var student = userRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); + request.get("/api/learning-path/" + learningPath.getId(), HttpStatus.FORBIDDEN, NgxLearningPathDTO.class); + } + + /** + * This only tests if the end point successfully retrieves the graph representation. The correctness of the response is tested in LearningPathServiceTest. + * + * @throws Exception the request failed + * @see de.tum.in.www1.artemis.service.LearningPathServiceTest + */ + @Test + @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") + void testGetNgxLearningPathAsStudent() throws Exception { + course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); + final var student = userRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); + request.get("/api/learning-path/" + learningPath.getId(), HttpStatus.OK, NgxLearningPathDTO.class); + } + + /** + * This only tests if the end point successfully retrieves the graph representation. The correctness of the response is tested in LearningPathServiceTest. + * + * @throws Exception the request failed + * @see de.tum.in.www1.artemis.service.LearningPathServiceTest + */ + @Test + @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") + void testGetNgxLearningPathAsInstructor() throws Exception { + course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); + final var student = userRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); + request.get("/api/learning-path/" + learningPath.getId(), HttpStatus.OK, NgxLearningPathDTO.class); + } } diff --git a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java index 12cb456454e3..a79cec2d0dd4 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java @@ -4,6 +4,7 @@ import java.time.ZonedDateTime; import java.util.*; +import java.util.stream.Collectors; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -11,14 +12,24 @@ import org.springframework.beans.factory.annotation.Autowired; import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.competency.CompetencyUtilService; import de.tum.in.www1.artemis.competency.LearningPathUtilService; import de.tum.in.www1.artemis.course.CourseFactory; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.Exercise; +import de.tum.in.www1.artemis.domain.competency.Competency; +import de.tum.in.www1.artemis.domain.competency.CompetencyRelation; +import de.tum.in.www1.artemis.domain.competency.LearningPath; +import de.tum.in.www1.artemis.domain.lecture.LectureUnit; +import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.lecture.LectureUtilService; import de.tum.in.www1.artemis.repository.CourseRepository; +import de.tum.in.www1.artemis.repository.LearningPathRepository; import de.tum.in.www1.artemis.security.SecurityUtils; import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.web.rest.dto.competency.LearningPathHealthDTO; +import de.tum.in.www1.artemis.web.rest.dto.competency.NgxLearningPathDTO; class LearningPathServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { @@ -39,6 +50,18 @@ class LearningPathServiceTest extends AbstractSpringIntegrationBambooBitbucketJi @Autowired CourseRepository courseRepository; + @Autowired + private CompetencyUtilService competencyUtilService; + + @Autowired + private LectureUtilService lectureUtilService; + + @Autowired + private ProgrammingExerciseUtilService programmingExerciseUtilService; + + @Autowired + private LearningPathRepository learningPathRepository; + private Course course; @BeforeEach @@ -84,4 +107,204 @@ void testHealthStatusMissing() { assertThat(healthStatus.missingLearningPaths()).isEqualTo(1); } } + + @Nested + class GenerateNgxRepresentationBaseTest { + + @Test + void testEmptyLearningPath() { + NgxLearningPathDTO expected = new NgxLearningPathDTO(Set.of(), Set.of(), Set.of()); + generateAndAssert(expected); + } + + @Test + void testEmptyCompetency() { + final var competency = competencyUtilService.createCompetency(course); + final var startNodeId = LearningPathService.getCompetencyStartNodeId(competency.getId()); + final var endNodeId = LearningPathService.getCompetencyEndNodeId(competency.getId()); + Set expectedNodes = getExpectedNodesOfEmptyCompetency(competency); + Set expectedEdges = Set.of(new NgxLearningPathDTO.Edge(LearningPathService.getDirectEdgeId(competency.getId()), startNodeId, endNodeId)); + Set expectedClusters = Set + .of(new NgxLearningPathDTO.Cluster(String.valueOf(competency.getId()), competency.getTitle(), Set.of(startNodeId, endNodeId))); + NgxLearningPathDTO expected = new NgxLearningPathDTO(expectedNodes, expectedEdges, expectedClusters); + generateAndAssert(expected); + } + + @Test + void testCompetencyWithLectureUnitAndExercise() { + var competency = competencyUtilService.createCompetency(course); + var lecture = lectureUtilService.createLecture(course, ZonedDateTime.now()); + final var lectureUnit = lectureUtilService.createTextUnit(); + lectureUtilService.addLectureUnitsToLecture(lecture, List.of(lectureUnit)); + competencyUtilService.linkLectureUnitToCompetency(competency, lectureUnit); + final var exercise = programmingExerciseUtilService.addProgrammingExerciseToCourse(course, false); + competencyUtilService.linkExerciseToCompetency(competency, exercise); + final var startNodeId = LearningPathService.getCompetencyStartNodeId(competency.getId()); + final var endNodeId = LearningPathService.getCompetencyEndNodeId(competency.getId()); + Set expectedNodes = getExpectedNodesOfEmptyCompetency(competency); + expectedNodes.add(getNodeForLectureUnit(competency, lectureUnit)); + expectedNodes.add(getNodeForExercise(competency, exercise)); + Set expectedEdges = Set.of( + new NgxLearningPathDTO.Edge(LearningPathService.getLectureUnitInEdgeId(competency.getId(), lectureUnit.getId()), startNodeId, + LearningPathService.getLectureUnitNodeId(competency.getId(), lectureUnit.getId())), + new NgxLearningPathDTO.Edge(LearningPathService.getLectureUnitOutEdgeId(competency.getId(), lectureUnit.getId()), + LearningPathService.getLectureUnitNodeId(competency.getId(), lectureUnit.getId()), endNodeId), + new NgxLearningPathDTO.Edge(LearningPathService.getExerciseInEdgeId(competency.getId(), exercise.getId()), startNodeId, + LearningPathService.getExerciseNodeId(competency.getId(), exercise.getId())), + new NgxLearningPathDTO.Edge(LearningPathService.getExerciseOutEdgeId(competency.getId(), exercise.getId()), + LearningPathService.getExerciseNodeId(competency.getId(), exercise.getId()), endNodeId)); + Set expectedClusters = Set.of(new NgxLearningPathDTO.Cluster(String.valueOf(competency.getId()), competency.getTitle(), + expectedNodes.stream().map(NgxLearningPathDTO.Node::id).collect(Collectors.toSet()))); + NgxLearningPathDTO expected = new NgxLearningPathDTO(expectedNodes, expectedEdges, expectedClusters); + generateAndAssert(expected); + } + + @Test + void testMultipleCompetencies() { + Competency[] competencies = { competencyUtilService.createCompetency(course), competencyUtilService.createCompetency(course), + competencyUtilService.createCompetency(course) }; + String[] startNodeIds = Arrays.stream(competencies).map(Competency::getId).map(LearningPathService::getCompetencyStartNodeId).toArray(String[]::new); + String[] endNodeIds = Arrays.stream(competencies).map(Competency::getId).map(LearningPathService::getCompetencyEndNodeId).toArray(String[]::new); + Set expectedNodes = new HashSet<>(); + Set expectedEdges = new HashSet<>(); + Set expectedClusters = new HashSet<>(); + for (int i = 0; i < competencies.length; i++) { + expectedNodes.addAll(getExpectedNodesOfEmptyCompetency(competencies[i])); + expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getDirectEdgeId(competencies[i].getId()), startNodeIds[i], endNodeIds[i])); + expectedClusters.add(new NgxLearningPathDTO.Cluster(String.valueOf(competencies[i].getId()), competencies[i].getTitle(), Set.of(startNodeIds[i], endNodeIds[i]))); + } + NgxLearningPathDTO expected = new NgxLearningPathDTO(expectedNodes, expectedEdges, expectedClusters); + generateAndAssert(expected); + } + } + + @Nested + class GenerateNgxRepresentationRelationTest { + + private Competency competency1; + + private Competency competency2; + + private Set expectedNodes; + + Set expectedEdges; + + Set expectedClusters; + + @BeforeEach + void setup() { + competency1 = competencyUtilService.createCompetency(course); + competency2 = competencyUtilService.createCompetency(course); + expectedNodes = new HashSet<>(); + expectedEdges = new HashSet<>(); + expectedClusters = new HashSet<>(); + addExpectedComponentsForEmptyCompetencies(competency1, competency2); + } + + void testSimpleRelation(CompetencyRelation.RelationType type) { + competencyUtilService.addRelation(competency1, type, competency2); + final var sourceNodeId = LearningPathService.getCompetencyEndNodeId(competency2.getId()); + final var targetNodeId = LearningPathService.getCompetencyStartNodeId(competency1.getId()); + expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getRelationEdgeId(sourceNodeId, targetNodeId), sourceNodeId, targetNodeId)); + NgxLearningPathDTO expected = new NgxLearningPathDTO(expectedNodes, expectedEdges, expectedClusters); + generateAndAssert(expected); + } + + @Test + void testSingleRelates() { + testSimpleRelation(CompetencyRelation.RelationType.RELATES); + } + + @Test + void testSingleAssumes() { + testSimpleRelation(CompetencyRelation.RelationType.ASSUMES); + } + + @Test + void testSingleExtends() { + testSimpleRelation(CompetencyRelation.RelationType.EXTENDS); + } + + @Test + void testSingleMatches() { + competencyUtilService.addRelation(competency1, CompetencyRelation.RelationType.MATCHES, competency2); + expectedNodes.add(new NgxLearningPathDTO.Node(LearningPathService.getMatchingClusterStartNodeId(0), NgxLearningPathDTO.NodeType.COMPETENCY_START, null, "")); + expectedNodes.add(new NgxLearningPathDTO.Node(LearningPathService.getMatchingClusterEndNodeId(0), NgxLearningPathDTO.NodeType.COMPETENCY_END, null, "")); + expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getInEdgeId(competency1.getId()), LearningPathService.getMatchingClusterStartNodeId(0), + LearningPathService.getCompetencyStartNodeId(competency1.getId()))); + expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getOutEdgeId(competency1.getId()), LearningPathService.getCompetencyEndNodeId(competency1.getId()), + LearningPathService.getMatchingClusterEndNodeId(0))); + expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getInEdgeId(competency2.getId()), LearningPathService.getMatchingClusterStartNodeId(0), + LearningPathService.getCompetencyStartNodeId(competency2.getId()))); + expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getOutEdgeId(competency2.getId()), LearningPathService.getCompetencyEndNodeId(competency2.getId()), + LearningPathService.getMatchingClusterEndNodeId(0))); + NgxLearningPathDTO expected = new NgxLearningPathDTO(expectedNodes, expectedEdges, expectedClusters); + generateAndAssert(expected); + } + + @Test + void testMatchesTransitive() { + var competency3 = competencyUtilService.createCompetency(course); + addExpectedComponentsForEmptyCompetencies(competency3); + + competencyUtilService.addRelation(competency1, CompetencyRelation.RelationType.MATCHES, competency2); + competencyUtilService.addRelation(competency2, CompetencyRelation.RelationType.MATCHES, competency3); + expectedNodes.add(new NgxLearningPathDTO.Node(LearningPathService.getMatchingClusterStartNodeId(0), NgxLearningPathDTO.NodeType.COMPETENCY_START, null, "")); + expectedNodes.add(new NgxLearningPathDTO.Node(LearningPathService.getMatchingClusterEndNodeId(0), NgxLearningPathDTO.NodeType.COMPETENCY_END, null, "")); + expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getInEdgeId(competency1.getId()), LearningPathService.getMatchingClusterStartNodeId(0), + LearningPathService.getCompetencyStartNodeId(competency1.getId()))); + expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getOutEdgeId(competency1.getId()), LearningPathService.getCompetencyEndNodeId(competency1.getId()), + LearningPathService.getMatchingClusterEndNodeId(0))); + expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getInEdgeId(competency2.getId()), LearningPathService.getMatchingClusterStartNodeId(0), + LearningPathService.getCompetencyStartNodeId(competency2.getId()))); + expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getOutEdgeId(competency2.getId()), LearningPathService.getCompetencyEndNodeId(competency2.getId()), + LearningPathService.getMatchingClusterEndNodeId(0))); + expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getInEdgeId(competency3.getId()), LearningPathService.getMatchingClusterStartNodeId(0), + LearningPathService.getCompetencyStartNodeId(competency3.getId()))); + expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getOutEdgeId(competency3.getId()), LearningPathService.getCompetencyEndNodeId(competency3.getId()), + LearningPathService.getMatchingClusterEndNodeId(0))); + NgxLearningPathDTO expected = new NgxLearningPathDTO(expectedNodes, expectedEdges, expectedClusters); + generateAndAssert(expected); + } + + private void addExpectedComponentsForEmptyCompetencies(Competency... competencies) { + for (var competency : competencies) { + expectedNodes.addAll(getExpectedNodesOfEmptyCompetency(competency)); + expectedEdges.add(new NgxLearningPathDTO.Edge(LearningPathService.getDirectEdgeId(competency.getId()), + LearningPathService.getCompetencyStartNodeId(competency.getId()), LearningPathService.getCompetencyEndNodeId(competency.getId()))); + expectedClusters.add(new NgxLearningPathDTO.Cluster(String.valueOf(competency.getId()), competency.getTitle(), + Set.of(LearningPathService.getCompetencyStartNodeId(competency.getId()), LearningPathService.getCompetencyEndNodeId(competency.getId())))); + } + } + } + + private void generateAndAssert(NgxLearningPathDTO expected) { + LearningPath learningPath = learningPathUtilService.createLearningPathInCourse(course); + learningPath = learningPathRepository.findWithEagerCompetenciesAndLearningObjectsAndCompletedUsersByIdElseThrow(learningPath.getId()); + NgxLearningPathDTO actual = learningPathService.generateNgxRepresentation(learningPath); + assertThat(actual).isNotNull(); + assertNgxRepEquals(actual, expected); + } + + private void assertNgxRepEquals(NgxLearningPathDTO was, NgxLearningPathDTO expected) { + assertThat(was.nodes()).as("correct nodes").containsExactlyInAnyOrderElementsOf(expected.nodes()); + assertThat(was.edges()).as("correct edges").containsExactlyInAnyOrderElementsOf(expected.edges()); + assertThat(was.clusters()).as("correct clusters").containsExactlyInAnyOrderElementsOf(expected.clusters()); + } + + private static Set getExpectedNodesOfEmptyCompetency(Competency competency) { + return new HashSet<>(Set.of( + new NgxLearningPathDTO.Node(LearningPathService.getCompetencyStartNodeId(competency.getId()), NgxLearningPathDTO.NodeType.COMPETENCY_START, competency.getId(), ""), + new NgxLearningPathDTO.Node(LearningPathService.getCompetencyEndNodeId(competency.getId()), NgxLearningPathDTO.NodeType.COMPETENCY_END, competency.getId(), ""))); + } + + private static NgxLearningPathDTO.Node getNodeForLectureUnit(Competency competency, LectureUnit lectureUnit) { + return new NgxLearningPathDTO.Node(LearningPathService.getLectureUnitNodeId(competency.getId(), lectureUnit.getId()), NgxLearningPathDTO.NodeType.LECTURE_UNIT, + lectureUnit.getId(), lectureUnit.getLecture().getId(), false, lectureUnit.getName()); + } + + private static NgxLearningPathDTO.Node getNodeForExercise(Competency competency, Exercise exercise) { + return new NgxLearningPathDTO.Node(LearningPathService.getExerciseNodeId(competency.getId(), exercise.getId()), NgxLearningPathDTO.NodeType.EXERCISE, exercise.getId(), + exercise.getTitle()); + } } From 5f347286af91bddd7b940633404f83a8c277a424 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Wed, 2 Aug 2023 16:03:27 +0200 Subject: [PATCH 127/215] fix code style --- .../tum/in/www1/artemis/service/LearningPathServiceTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java index 12cb456454e3..6f3b240fe457 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java @@ -34,10 +34,10 @@ class LearningPathServiceTest extends AbstractSpringIntegrationBambooBitbucketJi private CourseUtilService courseUtilService; @Autowired - UserUtilService userUtilService; + private UserUtilService userUtilService; @Autowired - CourseRepository courseRepository; + private CourseRepository courseRepository; private Course course; From b86beb216d34108e99ea250d5e34d29b70a25faa Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Wed, 2 Aug 2023 16:07:04 +0200 Subject: [PATCH 128/215] add feature toggle annotation to endpoints --- .../artemis/web/rest/LearningPathResource.java | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java index fca6892e86f5..05008a418ea9 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java @@ -9,12 +9,12 @@ import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.repository.CourseRepository; -import de.tum.in.www1.artemis.repository.LearningPathRepository; -import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.security.Role; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor; import de.tum.in.www1.artemis.service.AuthorizationCheckService; import de.tum.in.www1.artemis.service.LearningPathService; +import de.tum.in.www1.artemis.service.feature.Feature; +import de.tum.in.www1.artemis.service.feature.FeatureToggle; import de.tum.in.www1.artemis.web.rest.dto.PageableSearchDTO; import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; import de.tum.in.www1.artemis.web.rest.dto.competency.*; @@ -30,19 +30,12 @@ public class LearningPathResource { private final AuthorizationCheckService authorizationCheckService; - private final UserRepository userRepository; - private final LearningPathService learningPathService; - private final LearningPathRepository learningPathRepository; - - public LearningPathResource(CourseRepository courseRepository, AuthorizationCheckService authorizationCheckService, UserRepository userRepository, - LearningPathService learningPathService, LearningPathRepository learningPathRepository) { + public LearningPathResource(CourseRepository courseRepository, AuthorizationCheckService authorizationCheckService, LearningPathService learningPathService) { this.courseRepository = courseRepository; this.authorizationCheckService = authorizationCheckService; - this.userRepository = userRepository; this.learningPathService = learningPathService; - this.learningPathRepository = learningPathRepository; } /** @@ -52,6 +45,7 @@ public LearningPathResource(CourseRepository courseRepository, AuthorizationChec * @return the ResponseEntity with status 200 (OK) */ @PutMapping("/courses/{courseId}/learning-paths/enable") + @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastInstructor public ResponseEntity enableLearningPathsForCourse(@PathVariable Long courseId) { log.debug("REST request to enable learning paths for course with id: {}", courseId); @@ -75,6 +69,7 @@ public ResponseEntity enableLearningPathsForCourse(@PathVariable Long cour * @return the ResponseEntity with status 200 (OK) */ @PutMapping("/courses/{courseId}/learning-paths/generate-missing") + @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastInstructor public ResponseEntity generateMissingLearningPathsForCourse(@PathVariable Long courseId) { log.debug("REST request to generate missing learning paths for course with id: {}", courseId); @@ -95,6 +90,7 @@ public ResponseEntity generateMissingLearningPathsForCourse(@PathVariable * @return the ResponseEntity with status 200 (OK) and with body the desired page, sorted and matching the given query */ @GetMapping("/courses/{courseId}/learning-paths") + @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastInstructor public ResponseEntity> getLearningPathsOnPage(@PathVariable Long courseId, PageableSearchDTO search) { log.debug("REST request to get learning paths for course with id: {}", courseId); @@ -114,6 +110,7 @@ public ResponseEntity> getLea * @return the ResponseEntity with status 200 (OK) and with body the health status */ @GetMapping("/courses/{courseId}/learning-path-health") + @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastInstructor public ResponseEntity getHealthStatusForCourse(@PathVariable long courseId) { log.debug("REST request to get health status of learning paths in course with id: {}", courseId); From ea54c74aeeced4accd799bf19f86d459513a68cf Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Wed, 2 Aug 2023 16:09:50 +0200 Subject: [PATCH 129/215] add feature toggle annotation to endpoint --- .../www1/artemis/web/rest/LearningPathResource.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java index b8ab67dd4b71..309301101d3d 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java @@ -10,6 +10,8 @@ import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.competency.LearningPath; import de.tum.in.www1.artemis.repository.CourseRepository; +import de.tum.in.www1.artemis.repository.LearningPathRepository; +import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.security.Role; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent; @@ -34,10 +36,17 @@ public class LearningPathResource { private final LearningPathService learningPathService; - public LearningPathResource(CourseRepository courseRepository, AuthorizationCheckService authorizationCheckService, LearningPathService learningPathService) { + private final LearningPathRepository learningPathRepository; + + private final UserRepository userRepository; + + public LearningPathResource(CourseRepository courseRepository, AuthorizationCheckService authorizationCheckService, LearningPathService learningPathService, + LearningPathRepository learningPathRepository, UserRepository userRepository) { this.courseRepository = courseRepository; this.authorizationCheckService = authorizationCheckService; this.learningPathService = learningPathService; + this.learningPathRepository = learningPathRepository; + this.userRepository = userRepository; } /** @@ -132,6 +141,7 @@ public ResponseEntity getHealthStatusForCourse(@PathVaria * @return the ResponseEntity with status 200 (OK) and with body the ngx representation of the learning path */ @GetMapping("/learning-path/{learningPathId}") + @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastStudent public ResponseEntity getNgxLearningPath(@PathVariable Long learningPathId) { log.debug("REST request to get ngx representation of learning path with id: {}", learningPathId); From 47610965934eba6079c159ce1233edd4c2bc7c32 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 3 Aug 2023 11:21:13 +0200 Subject: [PATCH 130/215] remove unused and fix code style --- .../artemis/repository/UserRepository.java | 2 +- .../artemis/service/LearningPathService.java | 6 +- .../www1/artemis/web/rest/CourseResource.java | 15 --- .../web/rest/LearningPathResource.java | 3 +- .../manage/course-management.service.ts | 8 -- .../lecture-unit/lectureUnit.model.ts | 32 ------ .../overview/course-overview.component.html | 1 - .../app/overview/course-overview.component.ts | 2 - .../app/overview/courses-routing.module.ts | 4 - .../discussion-section.component.ts | 6 +- .../course-exercise-details.component.ts | 10 +- .../shared/layouts/navbar/navbar.component.ts | 3 - src/main/webapp/app/shared/shared.module.ts | 3 - .../sticky-popover.directive.ts | 105 ------------------ src/main/webapp/i18n/de/competency.json | 21 ---- .../webapp/i18n/de/student-dashboard.json | 1 - src/main/webapp/i18n/en/competency.json | 21 ---- .../webapp/i18n/en/student-dashboard.json | 1 - .../artemis/course/CourseTestService.java | 22 ---- ...rseBitbucketBambooJiraIntegrationTest.java | 6 - .../CourseGitlabJenkinsIntegrationTest.java | 6 - .../service/LearningPathServiceTest.java | 2 +- 22 files changed, 10 insertions(+), 270 deletions(-) delete mode 100644 src/main/webapp/app/shared/sticky-popover/sticky-popover.directive.ts diff --git a/src/main/java/de/tum/in/www1/artemis/repository/UserRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/UserRepository.java index 91ec0c29f870..4f9cd9a77b77 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/UserRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/UserRepository.java @@ -98,7 +98,7 @@ public interface UserRepository extends JpaRepository, JpaSpecificat Optional findOneWithGroupsAuthoritiesAndGuidedTourSettingsByLogin(String login); @EntityGraph(type = LOAD, attributePaths = { "learningPaths" }) - Optional findWithLearningPathsById(long id); + Optional findWithLearningPathsById(long userId); @Query("SELECT count(*) FROM User user WHERE user.isDeleted = false AND :#{#groupName} MEMBER OF user.groups") Long countByGroupsIsContaining(@Param("groupName") String groupName); diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index dba822bdd7fa..d7c1d572fe3d 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -1,6 +1,6 @@ package de.tum.in.www1.artemis.service; -import java.util.*; +import java.util.List; import java.util.stream.Collectors; import javax.validation.constraints.NotNull; @@ -27,9 +27,7 @@ * This includes *
      *
    • the generation of learning paths in courses,
    • - *
    • performing pageable searches for learning paths,
    • - *
    • generation of the Ngx representation of learning paths,
    • - *
    • and the computation of recommended learning objects for a specific learning path.
    • + *
    • and performing pageable searches for learning paths.
    • *
    */ @Service diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java index feaf469135a8..f6b073ef86c2 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java @@ -1212,19 +1212,4 @@ public ResponseEntity> addUsersToCourseGroup(@PathVariable Long List notFoundStudentsDtos = courseService.registerUsersForCourseGroup(courseId, studentDtos, courseGroup); return ResponseEntity.ok().body(notFoundStudentsDtos); } - - /** - * GET /courses/:courseId/learning-paths-enabled Get a course by id with eagerly loaded learning paths - * - * @param courseId the id of the course - * @return the course with eagerly loaded learning paths - */ - @GetMapping("courses/{courseId}/learning-paths-enabled") - @EnforceAtLeastInstructor - public ResponseEntity getCourseLearningPathsEnabled(@PathVariable Long courseId) { - log.debug("REST request to get if course has learning paths enabled : {}", courseId); - Course course = courseRepository.findByIdElseThrow(courseId); - authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null); - return ResponseEntity.ok(course.getLearningPathsEnabled()); - } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java index 05008a418ea9..8aedc991981f 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java @@ -17,7 +17,8 @@ import de.tum.in.www1.artemis.service.feature.FeatureToggle; import de.tum.in.www1.artemis.web.rest.dto.PageableSearchDTO; import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; -import de.tum.in.www1.artemis.web.rest.dto.competency.*; +import de.tum.in.www1.artemis.web.rest.dto.competency.LearningPathHealthDTO; +import de.tum.in.www1.artemis.web.rest.dto.competency.LearningPathPageableSearchDTO; import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException; @RestController diff --git a/src/main/webapp/app/course/manage/course-management.service.ts b/src/main/webapp/app/course/manage/course-management.service.ts index 5ff5ce30f36d..68cd769eaca9 100644 --- a/src/main/webapp/app/course/manage/course-management.service.ts +++ b/src/main/webapp/app/course/manage/course-management.service.ts @@ -685,12 +685,4 @@ export class CourseManagementService { course?.exams?.forEach((exam) => this.entityTitleService.setTitle(EntityType.EXAM, [exam.id], exam.title)); course?.organizations?.forEach((org) => this.entityTitleService.setTitle(EntityType.ORGANIZATION, [org.id], org.name)); } - - /** - * retrieves if the course with the given id has enabled learning paths - * @param courseId the id of the course - */ - getCourseLearningPathsEnabled(courseId: number): Observable> { - return this.http.get(`${this.resourceUrl}/${courseId}/learning-paths-enabled`, { observe: 'response' }); - } } diff --git a/src/main/webapp/app/entities/lecture-unit/lectureUnit.model.ts b/src/main/webapp/app/entities/lecture-unit/lectureUnit.model.ts index b878b4554020..647d462e0cb7 100644 --- a/src/main/webapp/app/entities/lecture-unit/lectureUnit.model.ts +++ b/src/main/webapp/app/entities/lecture-unit/lectureUnit.model.ts @@ -2,8 +2,6 @@ import { BaseEntity } from 'app/shared/model/base-entity'; import dayjs from 'dayjs/esm'; import { Lecture } from 'app/entities/lecture.model'; import { Competency } from 'app/entities/competency.model'; -import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { faDownload, faLink, faQuestion, faScroll, faVideo } from '@fortawesome/free-solid-svg-icons'; // IMPORTANT NOTICE: The following strings have to be consistent with // the ones defined in LectureUnit.java @@ -15,22 +13,6 @@ export enum LectureUnitType { ONLINE = 'online', } -export const lectureUnitIcons = { - [LectureUnitType.ATTACHMENT]: faDownload, - [LectureUnitType.EXERCISE]: faQuestion, - [LectureUnitType.TEXT]: faScroll, - [LectureUnitType.VIDEO]: faVideo, - [LectureUnitType.ONLINE]: faLink, -}; - -export const lectureUnitTooltips = { - [LectureUnitType.ATTACHMENT]: 'artemisApp.attachmentUnit.tooltip', - [LectureUnitType.EXERCISE]: '', - [LectureUnitType.TEXT]: 'artemisApp.textUnit.tooltip', - [LectureUnitType.VIDEO]: 'artemisApp.videoUnit.tooltip', - [LectureUnitType.ONLINE]: 'artemisApp.onlineUnit.tooltip', -}; - export abstract class LectureUnit implements BaseEntity { public id?: number; public name?: string; @@ -46,17 +28,3 @@ export abstract class LectureUnit implements BaseEntity { this.type = type; } } - -export function getIcon(lectureUnitType: LectureUnitType): IconProp { - if (!lectureUnitType) { - return faQuestion as IconProp; - } - return lectureUnitIcons[lectureUnitType] as IconProp; -} - -export function getIconTooltip(lectureUnitType: LectureUnitType) { - if (!lectureUnitType) { - return ''; - } - return lectureUnitTooltips[lectureUnitType]; -} diff --git a/src/main/webapp/app/overview/course-overview.component.html b/src/main/webapp/app/overview/course-overview.component.html index 5b34697f6bc8..937df274a472 100644 --- a/src/main/webapp/app/overview/course-overview.component.html +++ b/src/main/webapp/app/overview/course-overview.component.html @@ -1,7 +1,6 @@
    -
    diff --git a/src/main/webapp/app/overview/course-overview.component.ts b/src/main/webapp/app/overview/course-overview.component.ts index b1a8a54fb56c..835ac782f5b4 100644 --- a/src/main/webapp/app/overview/course-overview.component.ts +++ b/src/main/webapp/app/overview/course-overview.component.ts @@ -23,7 +23,6 @@ import { faFlag, faGraduationCap, faListAlt, - faNetworkWired, faPersonChalkboard, faSync, faTable, @@ -83,7 +82,6 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit faWrench = faWrench; faTable = faTable; faFlag = faFlag; - faNetworkWired = faNetworkWired; faListAlt = faListAlt; faChartBar = faChartBar; faFilePdf = faFilePdf; diff --git a/src/main/webapp/app/overview/courses-routing.module.ts b/src/main/webapp/app/overview/courses-routing.module.ts index 41cb383835a2..3272d689f660 100644 --- a/src/main/webapp/app/overview/courses-routing.module.ts +++ b/src/main/webapp/app/overview/courses-routing.module.ts @@ -65,10 +65,6 @@ const routes: Routes = [ path: 'competencies', loadChildren: () => import('./course-competencies/course-competencies.module').then((m) => m.CourseCompetenciesModule), }, - { - path: 'learning-path', - loadChildren: () => import('app/course/learning-paths/learning-paths.module').then((m) => m.ArtemisLearningPathsModule), - }, { path: 'discussion', loadChildren: () => import('./course-discussion/course-discussion.module').then((m) => m.CourseDiscussionModule), diff --git a/src/main/webapp/app/overview/discussion-section/discussion-section.component.ts b/src/main/webapp/app/overview/discussion-section/discussion-section.component.ts index edc9ed17430f..c4cc4e431a3f 100644 --- a/src/main/webapp/app/overview/discussion-section/discussion-section.component.ts +++ b/src/main/webapp/app/overview/discussion-section/discussion-section.component.ts @@ -70,11 +70,7 @@ export class DiscussionSectionComponent extends CourseDiscussionDirective implem this.course = this.exercise?.course ?? this.lecture?.course; this.metisService.setCourse(this.course); this.metisService.setPageType(this.pageType); - if (routeParams.params.courseId) { - this.setChannel(routeParams.params.courseId); - } else if (this.course?.id) { - this.setChannel(this.course.id); - } + this.setChannel(routeParams.params.courseId); this.createEmptyPost(); this.resetFormGroup(); }); diff --git a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts index e98a4222b6d9..ffb40ecdc9f9 100644 --- a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts +++ b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts @@ -69,7 +69,7 @@ export class CourseExerciseDetailsComponent implements OnInit, OnDestroy { readonly isMessagingEnabled = isMessagingEnabled; private currentUser: User; - public exerciseId: number; + private exerciseId: number; public courseId: number; public course: Course; public exercise?: Exercise; @@ -142,12 +142,8 @@ export class CourseExerciseDetailsComponent implements OnInit, OnDestroy { this.route.params.subscribe((params) => { const didExerciseChange = this.exerciseId !== parseInt(params['exerciseId'], 10); const didCourseChange = this.courseId !== parseInt(params['courseId'], 10); - if (params['exerciseId']) { - this.exerciseId = parseInt(params['exerciseId'], 10); - } - if (params['courseId']) { - this.courseId = parseInt(params['courseId'], 10); - } + this.exerciseId = parseInt(params['exerciseId'], 10); + this.courseId = parseInt(params['courseId'], 10); this.courseService.find(this.courseId).subscribe((courseResponse) => (this.course = courseResponse.body!)); this.accountService.identity().then((user: User) => { this.currentUser = user; diff --git a/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts b/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts index b0bb13855a86..44f093dba011 100644 --- a/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts +++ b/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts @@ -336,9 +336,6 @@ export class NavbarComponent implements OnInit, OnDestroy { exercises: 'artemisApp.courseOverview.menu.exercises', lectures: 'artemisApp.courseOverview.menu.lectures', competencies: 'artemisApp.courseOverview.menu.competencies', - learning_path: 'artemisApp.courseOverview.menu.learningPath', - lecture_unit: 'artemisApp.learningPath.breadcrumbs.lectureUnit', - exercise: 'artemisApp.learningPath.breadcrumbs.exercise', statistics: 'artemisApp.courseOverview.menu.statistics', discussion: 'artemisApp.metis.communication.label', messages: 'artemisApp.conversationsLayout.breadCrumbLabel', diff --git a/src/main/webapp/app/shared/shared.module.ts b/src/main/webapp/app/shared/shared.module.ts index 774efdf960a3..108a76462502 100644 --- a/src/main/webapp/app/shared/shared.module.ts +++ b/src/main/webapp/app/shared/shared.module.ts @@ -23,7 +23,6 @@ import { AssessmentWarningComponent } from 'app/assessment/assessment-warning/as import { JhiConnectionWarningComponent } from 'app/shared/connection-warning/connection-warning.component'; import { LoadingIndicatorContainerComponent } from 'app/shared/loading-indicator-container/loading-indicator-container.component'; import { CompetencySelectionComponent } from 'app/shared/competency-selection/competency-selection.component'; -import { StickyPopoverDirective } from 'app/shared/sticky-popover/sticky-popover.directive'; @NgModule({ imports: [ArtemisSharedLibsModule, ArtemisSharedCommonModule, ArtemisSharedPipesModule, RouterModule], @@ -48,7 +47,6 @@ import { StickyPopoverDirective } from 'app/shared/sticky-popover/sticky-popover ItemCountComponent, ConsistencyCheckComponent, AssessmentWarningComponent, - StickyPopoverDirective, ], exports: [ ArtemisSharedLibsModule, @@ -74,7 +72,6 @@ import { StickyPopoverDirective } from 'app/shared/sticky-popover/sticky-popover ConsistencyCheckComponent, AssessmentWarningComponent, CompetencySelectionComponent, - StickyPopoverDirective, ], }) export class ArtemisSharedModule {} diff --git a/src/main/webapp/app/shared/sticky-popover/sticky-popover.directive.ts b/src/main/webapp/app/shared/sticky-popover/sticky-popover.directive.ts deleted file mode 100644 index 4950c06f7937..000000000000 --- a/src/main/webapp/app/shared/sticky-popover/sticky-popover.directive.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { - ApplicationRef, - ChangeDetectorRef, - Directive, - ElementRef, - Inject, - Injector, - Input, - NgZone, - OnDestroy, - OnInit, - Renderer2, - TemplateRef, - ViewContainerRef, -} from '@angular/core'; - -import { DOCUMENT } from '@angular/common'; -import { NgbPopover, NgbPopoverConfig } from '@ng-bootstrap/ng-bootstrap'; -@Directive({ - selector: '[jhiStickyPopover]', -}) -export class StickyPopoverDirective extends NgbPopover implements OnInit, OnDestroy { - @Input() jhiStickyPopover: TemplateRef; - - popoverTitle: string; - - triggers: string; - container: string; - ngpPopover: TemplateRef; - canClosePopover: boolean; - - toggle(): void { - super.toggle(); - } - - isOpen(): boolean { - return super.isOpen(); - } - - constructor( - private _elRef: ElementRef, - private _render: Renderer2, - injector: Injector, - private viewContainerRef: ViewContainerRef, - config: NgbPopoverConfig, - ngZone: NgZone, - private changeRef: ChangeDetectorRef, - private applicationRef: ApplicationRef, - @Inject(DOCUMENT) _document: any, - ) { - super(_elRef, _render, injector, viewContainerRef, config, ngZone, _document, changeRef, applicationRef); - this.triggers = 'manual'; - this.popoverTitle = ''; - this.container = 'body'; - } - - ngOnInit(): void { - super.ngOnInit(); - this.ngbPopover = this.jhiStickyPopover; - - this._render.listen(this._elRef.nativeElement, 'mouseenter', () => { - this.canClosePopover = true; - this.open(); - }); - - this._render.listen(this._elRef.nativeElement, 'mouseleave', () => { - setTimeout(() => { - if (this.canClosePopover) { - this.close(); - } - }, 100); - }); - - this._render.listen(this._elRef.nativeElement, 'click', () => { - this.close(); - }); - } - - ngOnDestroy(): void { - super.ngOnDestroy(); - } - - open() { - super.open(); - setTimeout(() => { - const popover = window.document.querySelector('.popover'); - this._render.listen(popover, 'mouseover', () => { - this.canClosePopover = false; - }); - - this._render.listen(popover, 'mouseout', () => { - this.canClosePopover = true; - setTimeout(() => { - if (this.canClosePopover) { - this.close(); - } - }, 0); - }); - }, 0); - } - - close() { - super.close(); - } -} diff --git a/src/main/webapp/i18n/de/competency.json b/src/main/webapp/i18n/de/competency.json index 4f4bc84ecdf9..62d59e2cd035 100644 --- a/src/main/webapp/i18n/de/competency.json +++ b/src/main/webapp/i18n/de/competency.json @@ -151,10 +151,6 @@ }, "learningPath": { "learningPathButton": "Lernpfade", - "breadcrumbs": { - "lectureUnit": "Vorlesungseinheit", - "exercise": "Aufgabe" - }, "manageLearningPaths": { "title": "Lernpfadmanagement", "health": { @@ -173,24 +169,7 @@ "name": "Name", "login": "Login", "progress": "Fortschritt" - }, - "progressNav": { - "header": "Lernpfad", - "refresh": "Aktualisieren", - "center": "Zentrieren" } - }, - "sideBar": { - "hide": "Lernpfad ausblenden", - "show": "Lernpfad einblenden", - "header": "Lernpfad" - }, - "participate": { - "noTaskSelected": "Aktuell hast du keine Vorlesungseinheit oder Aufgabe ausgewählt.", - "prev": "Zurück", - "prevHint": "Zurück zur letzen Vorlesungseinheit oder Aufgabe", - "next": "Weiter", - "nextHint": "Weiter zur nächten Empfehlung" } } } diff --git a/src/main/webapp/i18n/de/student-dashboard.json b/src/main/webapp/i18n/de/student-dashboard.json index bc34950a6fe6..d2d0d5d7afd6 100644 --- a/src/main/webapp/i18n/de/student-dashboard.json +++ b/src/main/webapp/i18n/de/student-dashboard.json @@ -33,7 +33,6 @@ "statistics": "Statistiken", "lectures": "Vorlesungen", "competencies": "Kompetenzen", - "learningPath": "Lernpfad", "tutorialGroups": "Tutorien", "exams": "Klausuren", "testExam": "Testklausur", diff --git a/src/main/webapp/i18n/en/competency.json b/src/main/webapp/i18n/en/competency.json index 73d444187ab1..fbc7c96a12aa 100644 --- a/src/main/webapp/i18n/en/competency.json +++ b/src/main/webapp/i18n/en/competency.json @@ -150,10 +150,6 @@ }, "learningPath": { "learningPathButton": "Learning Paths", - "breadcrumbs": { - "lectureUnit": "Lecture Unit", - "exercise": "Exercise" - }, "manageLearningPaths": { "title": "Learning Path Management", "health": { @@ -172,24 +168,7 @@ "name": "Name", "login": "Login", "progress": "Progress" - }, - "progressNav": { - "header": "Learning Path", - "refresh": "Refresh", - "center": "Center view" } - }, - "sideBar": { - "hide": "Hide Learning Path", - "show": "Show Learning Path", - "header": "Learning Path" - }, - "participate": { - "noTaskSelected": "Currently you have no lecture unit or exercise selected.", - "prev": "Previous", - "prevHint": "Return to your previous task", - "next": "Next", - "nextHint": "Go to next recommended task" } } } diff --git a/src/main/webapp/i18n/en/student-dashboard.json b/src/main/webapp/i18n/en/student-dashboard.json index b0212682ffba..bd38b3313712 100644 --- a/src/main/webapp/i18n/en/student-dashboard.json +++ b/src/main/webapp/i18n/en/student-dashboard.json @@ -34,7 +34,6 @@ "lectures": "Lectures", "tutorialGroups": "Tutorials", "competencies": "Competencies", - "learningPath": "Learning Path", "exams": "Exams", "testExam": "Test Exam", "communication": "Communication", diff --git a/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java b/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java index 6f85679b4ae3..1e5eae1968d1 100644 --- a/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java +++ b/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java @@ -3129,26 +3129,4 @@ public void testEditCourseRemoveExistingIcon() throws Exception { private String getUpdateOnlineCourseConfigurationPath(String courseId) { return "/api/courses/" + courseId + "/onlineCourseConfiguration"; } - - // Test - public void testGetCourseLearningPathsEnabled_AsInstructor() throws Exception { - String testSuffix = "getlearningpathsenabled"; - adjustUserGroupsToCustomGroups(testSuffix); - - var course1 = courseUtilService.createCourse(); - adjustCourseGroups(course1, testSuffix); - course1.setLearningPathsEnabled(true); - course1 = courseRepo.save(course1); - - var course2 = courseUtilService.createCourse(); - adjustCourseGroups(course2, testSuffix); - course2.setLearningPathsEnabled(false); - course2 = courseRepo.save(course2); - - final var result1 = request.get("/api/courses/" + course1.getId() + "/learning-paths-enabled", HttpStatus.OK, Boolean.class); - assertThat(result1).isTrue(); - - final var result2 = request.get("/api/courses/" + course2.getId() + "/learning-paths-enabled", HttpStatus.OK, Boolean.class); - assertThat(result2).isFalse(); - } } diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseBitbucketBambooJiraIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseBitbucketBambooJiraIntegrationTest.java index 1c24d7cc0243..cf94dabc272d 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseBitbucketBambooJiraIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseBitbucketBambooJiraIntegrationTest.java @@ -918,10 +918,4 @@ void testUpdateValidOnlineCourseConfiguration() throws Exception { void testEditCourseRemoveExistingIcon() throws Exception { courseTestService.testEditCourseRemoveExistingIcon(); } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testGetCourseLearningPathsEnabled_AsInstructor() throws Exception { - courseTestService.testGetCourseLearningPathsEnabled_AsInstructor(); - } } diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseGitlabJenkinsIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseGitlabJenkinsIntegrationTest.java index 690edef3a80c..f859fca4b856 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseGitlabJenkinsIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseGitlabJenkinsIntegrationTest.java @@ -1036,10 +1036,4 @@ void testUpdateValidOnlineCourseConfiguration() throws Exception { void testEditCourseRemoveExistingIcon() throws Exception { courseTestService.testEditCourseRemoveExistingIcon(); } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testGetCourseLearningPathsEnabled_AsInstructor() throws Exception { - courseTestService.testGetCourseLearningPathsEnabled_AsInstructor(); - } } diff --git a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java index 6f3b240fe457..f33149e252c9 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java @@ -3,7 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import java.time.ZonedDateTime; -import java.util.*; +import java.util.HashSet; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; From 8bbc0c2751f1685ec63154bb19c46d4a0f0e1cdc Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 3 Aug 2023 12:06:50 +0200 Subject: [PATCH 131/215] add sticky popover directive --- src/main/webapp/app/shared/shared.module.ts | 3 + .../sticky-popover.directive.ts | 105 ++++++++++++++++++ .../sticky-popover.directive.spec.ts | 60 ++++++++++ 3 files changed, 168 insertions(+) create mode 100644 src/main/webapp/app/shared/sticky-popover/sticky-popover.directive.ts create mode 100644 src/test/javascript/spec/directive/sticky-popover.directive.spec.ts diff --git a/src/main/webapp/app/shared/shared.module.ts b/src/main/webapp/app/shared/shared.module.ts index 108a76462502..774efdf960a3 100644 --- a/src/main/webapp/app/shared/shared.module.ts +++ b/src/main/webapp/app/shared/shared.module.ts @@ -23,6 +23,7 @@ import { AssessmentWarningComponent } from 'app/assessment/assessment-warning/as import { JhiConnectionWarningComponent } from 'app/shared/connection-warning/connection-warning.component'; import { LoadingIndicatorContainerComponent } from 'app/shared/loading-indicator-container/loading-indicator-container.component'; import { CompetencySelectionComponent } from 'app/shared/competency-selection/competency-selection.component'; +import { StickyPopoverDirective } from 'app/shared/sticky-popover/sticky-popover.directive'; @NgModule({ imports: [ArtemisSharedLibsModule, ArtemisSharedCommonModule, ArtemisSharedPipesModule, RouterModule], @@ -47,6 +48,7 @@ import { CompetencySelectionComponent } from 'app/shared/competency-selection/co ItemCountComponent, ConsistencyCheckComponent, AssessmentWarningComponent, + StickyPopoverDirective, ], exports: [ ArtemisSharedLibsModule, @@ -72,6 +74,7 @@ import { CompetencySelectionComponent } from 'app/shared/competency-selection/co ConsistencyCheckComponent, AssessmentWarningComponent, CompetencySelectionComponent, + StickyPopoverDirective, ], }) export class ArtemisSharedModule {} diff --git a/src/main/webapp/app/shared/sticky-popover/sticky-popover.directive.ts b/src/main/webapp/app/shared/sticky-popover/sticky-popover.directive.ts new file mode 100644 index 000000000000..4950c06f7937 --- /dev/null +++ b/src/main/webapp/app/shared/sticky-popover/sticky-popover.directive.ts @@ -0,0 +1,105 @@ +import { + ApplicationRef, + ChangeDetectorRef, + Directive, + ElementRef, + Inject, + Injector, + Input, + NgZone, + OnDestroy, + OnInit, + Renderer2, + TemplateRef, + ViewContainerRef, +} from '@angular/core'; + +import { DOCUMENT } from '@angular/common'; +import { NgbPopover, NgbPopoverConfig } from '@ng-bootstrap/ng-bootstrap'; +@Directive({ + selector: '[jhiStickyPopover]', +}) +export class StickyPopoverDirective extends NgbPopover implements OnInit, OnDestroy { + @Input() jhiStickyPopover: TemplateRef; + + popoverTitle: string; + + triggers: string; + container: string; + ngpPopover: TemplateRef; + canClosePopover: boolean; + + toggle(): void { + super.toggle(); + } + + isOpen(): boolean { + return super.isOpen(); + } + + constructor( + private _elRef: ElementRef, + private _render: Renderer2, + injector: Injector, + private viewContainerRef: ViewContainerRef, + config: NgbPopoverConfig, + ngZone: NgZone, + private changeRef: ChangeDetectorRef, + private applicationRef: ApplicationRef, + @Inject(DOCUMENT) _document: any, + ) { + super(_elRef, _render, injector, viewContainerRef, config, ngZone, _document, changeRef, applicationRef); + this.triggers = 'manual'; + this.popoverTitle = ''; + this.container = 'body'; + } + + ngOnInit(): void { + super.ngOnInit(); + this.ngbPopover = this.jhiStickyPopover; + + this._render.listen(this._elRef.nativeElement, 'mouseenter', () => { + this.canClosePopover = true; + this.open(); + }); + + this._render.listen(this._elRef.nativeElement, 'mouseleave', () => { + setTimeout(() => { + if (this.canClosePopover) { + this.close(); + } + }, 100); + }); + + this._render.listen(this._elRef.nativeElement, 'click', () => { + this.close(); + }); + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + } + + open() { + super.open(); + setTimeout(() => { + const popover = window.document.querySelector('.popover'); + this._render.listen(popover, 'mouseover', () => { + this.canClosePopover = false; + }); + + this._render.listen(popover, 'mouseout', () => { + this.canClosePopover = true; + setTimeout(() => { + if (this.canClosePopover) { + this.close(); + } + }, 0); + }); + }, 0); + } + + close() { + super.close(); + } +} diff --git a/src/test/javascript/spec/directive/sticky-popover.directive.spec.ts b/src/test/javascript/spec/directive/sticky-popover.directive.spec.ts new file mode 100644 index 000000000000..13262c4fcf9a --- /dev/null +++ b/src/test/javascript/spec/directive/sticky-popover.directive.spec.ts @@ -0,0 +1,60 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { Component, DebugElement } from '@angular/core'; +import { ArtemisTestModule } from '../test.module'; +import { By } from '@angular/platform-browser'; +import { StickyPopoverDirective } from 'app/shared/sticky-popover/sticky-popover.directive'; + +@Component({ + template: '
    some content', +}) +class StickyPopoverComponent { + pattern: string; +} + +describe('StickyPopoverDirective', () => { + let fixture: ComponentFixture; + let debugDirective: DebugElement; + let directive: StickyPopoverDirective; + let openStub: jest.SpyInstance; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule], + declarations: [StickyPopoverDirective, StickyPopoverComponent], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(StickyPopoverComponent); + debugDirective = fixture.debugElement.query(By.directive(StickyPopoverDirective)); + directive = debugDirective.injector.get(StickyPopoverDirective); + openStub = jest.spyOn(directive, 'open'); + fixture.detectChanges(); + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should open on hover', fakeAsync(() => { + fixture.whenStable(); + const div = fixture.debugElement.query(By.css('div')); + expect(div).not.toBeNull(); + div.nativeElement.dispatchEvent(new MouseEvent('mouseenter')); + tick(10); + expect(openStub).toHaveBeenCalledOnce(); + expect(directive.isOpen()).toBeTruthy(); + const span = fixture.debugElement.query(By.css('span')); + expect(span).not.toBeNull(); + })); + + it('should display content on hover', fakeAsync(() => { + fixture.whenStable(); + const div = fixture.debugElement.query(By.css('div')); + expect(div).not.toBeNull(); + div.nativeElement.dispatchEvent(new MouseEvent('mouseenter')); + tick(10); + const span = fixture.debugElement.query(By.css('span')); + expect(span).not.toBeNull(); + })); +}); From abf8eac4df5850aeb3fa20a5735316a9e1de5dd5 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 3 Aug 2023 12:13:21 +0200 Subject: [PATCH 132/215] add icon and hint retrieval to lecture unit model --- .../lecture-unit/lectureUnit.model.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/main/webapp/app/entities/lecture-unit/lectureUnit.model.ts b/src/main/webapp/app/entities/lecture-unit/lectureUnit.model.ts index 647d462e0cb7..b878b4554020 100644 --- a/src/main/webapp/app/entities/lecture-unit/lectureUnit.model.ts +++ b/src/main/webapp/app/entities/lecture-unit/lectureUnit.model.ts @@ -2,6 +2,8 @@ import { BaseEntity } from 'app/shared/model/base-entity'; import dayjs from 'dayjs/esm'; import { Lecture } from 'app/entities/lecture.model'; import { Competency } from 'app/entities/competency.model'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { faDownload, faLink, faQuestion, faScroll, faVideo } from '@fortawesome/free-solid-svg-icons'; // IMPORTANT NOTICE: The following strings have to be consistent with // the ones defined in LectureUnit.java @@ -13,6 +15,22 @@ export enum LectureUnitType { ONLINE = 'online', } +export const lectureUnitIcons = { + [LectureUnitType.ATTACHMENT]: faDownload, + [LectureUnitType.EXERCISE]: faQuestion, + [LectureUnitType.TEXT]: faScroll, + [LectureUnitType.VIDEO]: faVideo, + [LectureUnitType.ONLINE]: faLink, +}; + +export const lectureUnitTooltips = { + [LectureUnitType.ATTACHMENT]: 'artemisApp.attachmentUnit.tooltip', + [LectureUnitType.EXERCISE]: '', + [LectureUnitType.TEXT]: 'artemisApp.textUnit.tooltip', + [LectureUnitType.VIDEO]: 'artemisApp.videoUnit.tooltip', + [LectureUnitType.ONLINE]: 'artemisApp.onlineUnit.tooltip', +}; + export abstract class LectureUnit implements BaseEntity { public id?: number; public name?: string; @@ -28,3 +46,17 @@ export abstract class LectureUnit implements BaseEntity { this.type = type; } } + +export function getIcon(lectureUnitType: LectureUnitType): IconProp { + if (!lectureUnitType) { + return faQuestion as IconProp; + } + return lectureUnitIcons[lectureUnitType] as IconProp; +} + +export function getIconTooltip(lectureUnitType: LectureUnitType) { + if (!lectureUnitType) { + return ''; + } + return lectureUnitTooltips[lectureUnitType]; +} From e2ed5b7d22da8d4b75eeda6031506bc5025712e3 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 3 Aug 2023 12:15:41 +0200 Subject: [PATCH 133/215] add translations --- src/main/webapp/i18n/de/competency.json | 5 +++++ src/main/webapp/i18n/en/competency.json | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/main/webapp/i18n/de/competency.json b/src/main/webapp/i18n/de/competency.json index 62d59e2cd035..6543d4a798a8 100644 --- a/src/main/webapp/i18n/de/competency.json +++ b/src/main/webapp/i18n/de/competency.json @@ -169,6 +169,11 @@ "name": "Name", "login": "Login", "progress": "Fortschritt" + }, + "progressNav": { + "header": "Lernpfad", + "refresh": "Aktualisieren", + "center": "Zentrieren" } } } diff --git a/src/main/webapp/i18n/en/competency.json b/src/main/webapp/i18n/en/competency.json index fbc7c96a12aa..ba6dc5f20b2e 100644 --- a/src/main/webapp/i18n/en/competency.json +++ b/src/main/webapp/i18n/en/competency.json @@ -168,6 +168,11 @@ "name": "Name", "login": "Login", "progress": "Progress" + }, + "progressNav": { + "header": "Learning Path", + "refresh": "Refresh", + "center": "Center view" } } } From 3b15b298470bb45de24037e2fcdfa5170c21f66d Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 3 Aug 2023 12:19:14 +0200 Subject: [PATCH 134/215] add missing import --- .../de/tum/in/www1/artemis/web/rest/LearningPathResource.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java index 3f59d4131896..a48f9454f5ed 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java @@ -23,6 +23,7 @@ import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; import de.tum.in.www1.artemis.web.rest.dto.competency.LearningPathHealthDTO; import de.tum.in.www1.artemis.web.rest.dto.competency.LearningPathPageableSearchDTO; +import de.tum.in.www1.artemis.web.rest.dto.competency.NgxLearningPathDTO; import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException; @RestController From 06254639cd2778512769b1569e0938804d3dd375 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 3 Aug 2023 12:26:03 +0200 Subject: [PATCH 135/215] add dependencies for ngx graph --- package-lock.json | 10 ---------- package.json | 4 +++- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index d9ba139fd300..346d4f062f3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5941,11 +5941,6 @@ "d3-time-format": "2 - 3" } }, - "node_modules/@swimlane/ngx-graph/node_modules/d3-selection": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.2.tgz", - "integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==" - }, "node_modules/@swimlane/ngx-graph/node_modules/d3-shape": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", @@ -20114,11 +20109,6 @@ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" }, - "node_modules/webcola/node_modules/d3-selection": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.2.tgz", - "integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==" - }, "node_modules/webcola/node_modules/d3-shape": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", diff --git a/package.json b/package.json index 35913127237d..f55e4b165d4e 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,9 @@ "@swimlane/ngx-graph": { "d3-color": "^3.1.0", "d3-interpolate": "^3.0.1", - "d3-brush": "^3.0.0" + "d3-transition": "^3.0.1", + "d3-brush": "^3.0.0", + "d3-selection": "^3.0.0" }, "semver": "7.5.4", "word-wrap": "1.2.3", From 0c315a96ac93c630d5671ed6f2e2baeee98ca04c Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Fri, 4 Aug 2023 13:02:32 +0200 Subject: [PATCH 136/215] add suggestions by Tobias --- .../web/rest/LearningPathResource.java | 24 +++++++++---------- .../competency/LearningPathUtilService.java | 3 --- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java index 8aedc991981f..bfef936c606d 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java @@ -22,7 +22,7 @@ import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException; @RestController -@RequestMapping("/api") +@RequestMapping("api/") public class LearningPathResource { private final Logger log = LoggerFactory.getLogger(LearningPathResource.class); @@ -40,15 +40,15 @@ public LearningPathResource(CourseRepository courseRepository, AuthorizationChec } /** - * PUT /courses/:courseId/learning-paths/enable : Enables and generates learning paths for the course + * PUT courses/:courseId/learning-paths/enable : Enables and generates learning paths for the course * * @param courseId the id of the course for which the learning paths should be enabled * @return the ResponseEntity with status 200 (OK) */ - @PutMapping("/courses/{courseId}/learning-paths/enable") + @PutMapping("courses/{courseId}/learning-paths/enable") @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastInstructor - public ResponseEntity enableLearningPathsForCourse(@PathVariable Long courseId) { + public ResponseEntity enableLearningPathsForCourse(@PathVariable long courseId) { log.debug("REST request to enable learning paths for course with id: {}", courseId); Course course = courseRepository.findWithEagerCompetenciesByIdElseThrow(courseId); authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null); @@ -64,15 +64,15 @@ public ResponseEntity enableLearningPathsForCourse(@PathVariable Long cour } /** - * PUT /courses/:courseId/learning-paths/generate-missing : Generates missing learning paths for the course + * PUT courses/:courseId/learning-paths/generate-missing : Generates missing learning paths for the course * * @param courseId the id of the course for which the learning paths should be created * @return the ResponseEntity with status 200 (OK) */ - @PutMapping("/courses/{courseId}/learning-paths/generate-missing") + @PutMapping("courses/{courseId}/learning-paths/generate-missing") @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastInstructor - public ResponseEntity generateMissingLearningPathsForCourse(@PathVariable Long courseId) { + public ResponseEntity generateMissingLearningPathsForCourse(@PathVariable long courseId) { log.debug("REST request to generate missing learning paths for course with id: {}", courseId); Course course = courseRepository.findWithEagerCompetenciesByIdElseThrow(courseId); authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null); @@ -84,16 +84,16 @@ public ResponseEntity generateMissingLearningPathsForCourse(@PathVariable } /** - * GET /courses/:courseId/learning-paths : Gets all the learning paths of a course. The result is pageable. + * GET courses/:courseId/learning-paths : Gets all the learning paths of a course. The result is pageable. * * @param courseId the id of the course for which the learning paths should be fetched * @param search the pageable search containing the page size, page number and query string * @return the ResponseEntity with status 200 (OK) and with body the desired page, sorted and matching the given query */ - @GetMapping("/courses/{courseId}/learning-paths") + @GetMapping("courses/{courseId}/learning-paths") @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastInstructor - public ResponseEntity> getLearningPathsOnPage(@PathVariable Long courseId, PageableSearchDTO search) { + public ResponseEntity> getLearningPathsOnPage(@PathVariable long courseId, PageableSearchDTO search) { log.debug("REST request to get learning paths for course with id: {}", courseId); Course course = courseRepository.findByIdElseThrow(courseId); authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null); @@ -105,12 +105,12 @@ public ResponseEntity> getLea } /** - * GET /courses/:courseId/learning-path-health : Gets the health status of learning paths for the course. + * GET courses/:courseId/learning-path-health : Gets the health status of learning paths for the course. * * @param courseId the id of the course for which the health status should be fetched * @return the ResponseEntity with status 200 (OK) and with body the health status */ - @GetMapping("/courses/{courseId}/learning-path-health") + @GetMapping("courses/{courseId}/learning-path-health") @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastInstructor public ResponseEntity getHealthStatusForCourse(@PathVariable long courseId) { diff --git a/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java b/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java index ab3c16048d8a..4cf709c0c51f 100644 --- a/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/competency/LearningPathUtilService.java @@ -30,9 +30,6 @@ public class LearningPathUtilService { @Autowired private CompetencyRepository competencyRepository; - @Autowired - UserRepository userRepository; - /** * Enable and generate learning paths for course. * From a9e0321d1371ae5b7bacca773ab8a52d899deb3f Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Fri, 4 Aug 2023 13:18:58 +0200 Subject: [PATCH 137/215] add tests for error alert in learning path management --- .../artemis/service/LearningPathService.java | 2 +- ...learning-path-management.component.spec.ts | 38 ++++++++++++++++++- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index d7c1d572fe3d..9fb73dd2182a 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -167,7 +167,7 @@ private void updateLearningPathProgress(@NotNull LearningPath learningPath) { * @param course the course for which the health status should be generated * @return dto containing the health status and additional information (missing learning paths) if needed */ - public LearningPathHealthDTO getHealthStatusForCourse(Course course) { + public LearningPathHealthDTO getHealthStatusForCourse(@NotNull Course course) { if (!course.getLearningPathsEnabled()) { return new LearningPathHealthDTO(LearningPathHealthDTO.HealthStatus.DISABLED); } diff --git a/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts b/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts index e2ab1499590a..bf89f3f55e26 100644 --- a/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts @@ -10,15 +10,18 @@ import { ButtonComponent } from 'app/shared/components/button.component'; import { NgbPagination } from '@ng-bootstrap/ng-bootstrap'; import { SortByDirective } from 'app/shared/sort/sort-by.directive'; import { SortDirective } from 'app/shared/sort/sort.directive'; -import { of } from 'rxjs'; +import { of, throwError } from 'rxjs'; import { ActivatedRoute } from '@angular/router'; -import { HttpResponse } from '@angular/common/http'; +import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { LearningPathService } from 'app/course/learning-paths/learning-path.service'; import { HealthStatus, LearningPathHealthDTO } from 'app/entities/competency/learning-path-health.model'; +import { AlertService } from 'app/core/util/alert.service'; describe('LearningPathManagementComponent', () => { let fixture: ComponentFixture; let comp: LearningPathManagementComponent; + let alertService: AlertService; + let alertServiceStub: jest.SpyInstance; let pagingService: LearningPathPagingService; let sortService: SortService; let searchForLearningPathsStub: jest.SpyInstance; @@ -53,6 +56,8 @@ describe('LearningPathManagementComponent', () => { .then(() => { fixture = TestBed.createComponent(LearningPathManagementComponent); comp = fixture.componentInstance; + alertService = TestBed.inject(AlertService); + alertServiceStub = jest.spyOn(alertService, 'error'); pagingService = TestBed.inject(LearningPathPagingService); sortService = TestBed.inject(SortService); searchForLearningPathsStub = jest.spyOn(pagingService, 'searchForLearningPaths'); @@ -107,6 +112,15 @@ describe('LearningPathManagementComponent', () => { }); })); + it('should alert error if loading health status fails', fakeAsync(() => { + const error = { status: 404 }; + getHealthStatusForCourseStub.mockReturnValue(throwError(() => new HttpErrorResponse(error))); + fixture.detectChanges(); + comp.ngOnInit(); + expect(getHealthStatusForCourseStub).toHaveBeenCalledWith(courseId); + expect(alertServiceStub).toHaveBeenCalledOnce(); + })); + it('should enable learning paths and load data', fakeAsync(() => { const healthDisabled = new LearningPathHealthDTO(HealthStatus.DISABLED); getHealthStatusForCourseStub.mockReturnValueOnce(of(new HttpResponse({ body: healthDisabled }))).mockReturnValueOnce(of(new HttpResponse({ body: health }))); @@ -120,6 +134,16 @@ describe('LearningPathManagementComponent', () => { expect(comp.health).toEqual(health); })); + it('should alert error if enable learning paths fails', fakeAsync(() => { + const error = { status: 404 }; + enableLearningPathsStub.mockReturnValue(throwError(() => new HttpErrorResponse(error))); + fixture.detectChanges(); + comp.ngOnInit(); + comp.enableLearningPaths(); + expect(enableLearningPathsStub).toHaveBeenCalledWith(courseId); + expect(alertServiceStub).toHaveBeenCalledOnce(); + })); + it('should generate missing learning paths and load data', fakeAsync(() => { const healthMissing = new LearningPathHealthDTO(HealthStatus.MISSING); getHealthStatusForCourseStub.mockReturnValueOnce(of(new HttpResponse({ body: healthMissing }))).mockReturnValueOnce(of(new HttpResponse({ body: health }))); @@ -133,6 +157,16 @@ describe('LearningPathManagementComponent', () => { expect(comp.health).toEqual(health); })); + it('should alert error if generate missing learning paths fails', fakeAsync(() => { + const error = { status: 404 }; + generateMissingLearningPathsForCourseStub.mockReturnValue(throwError(() => new HttpErrorResponse(error))); + fixture.detectChanges(); + comp.ngOnInit(); + comp.generateMissing(); + expect(generateMissingLearningPathsForCourseStub).toHaveBeenCalledWith(courseId); + expect(alertServiceStub).toHaveBeenCalledOnce(); + })); + it('should set content to paging result on sort', fakeAsync(() => { expect(comp.listSorting).toBeTrue(); setStateAndCallOnInit(() => { From e5471551db586bcdb4cce51e773277a5e6159462 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Fri, 4 Aug 2023 18:13:33 +0200 Subject: [PATCH 138/215] add suggenstions from review and testing session --- .../artemis/service/feature/FeatureToggleService.java | 11 +++++++++-- .../course-management-tab-bar.component.html | 2 +- .../artemis/lecture/LearningPathIntegrationTest.java | 4 +--- .../course-management-tab-bar.component.spec.ts | 2 ++ 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/feature/FeatureToggleService.java b/src/main/java/de/tum/in/www1/artemis/service/feature/FeatureToggleService.java index 544fe7bcac8e..8989f6ce1610 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/feature/FeatureToggleService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/feature/FeatureToggleService.java @@ -25,10 +25,17 @@ public FeatureToggleService(WebsocketMessagingService websocketMessagingService, features = hazelcastInstance.getMap("features"); // Features that are neither enabled nor disabled should be enabled by default - // This ensures that all features are enabled once the system starts up + // This ensures that all features (except learning paths) are enabled once the system starts up for (Feature feature : Feature.values()) { if (!features.containsKey(feature)) { - features.put(feature, true); + if (feature == Feature.LearningPaths) { + // disable learning paths per default + // TODO: remove this once learning paths are deliverable + features.put(feature, false); + } + else { + features.put(feature, true); + } } } } diff --git a/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html b/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html index 632188f51f4d..7bd8437a1a16 100644 --- a/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html +++ b/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html @@ -44,7 +44,7 @@
    gradingCriteria = exerciseUtilService.addGradingInstructionsToExercise(textExercise); gradingCriterionRepository.saveAll(gradingCriteria); participationUtilService.addAssessmentWithFeedbackWithGradingInstructionsForExercise(textExercise, STUDENT_OF_COURSE); diff --git a/src/test/javascript/spec/component/course/course-management-tab-bar.component.spec.ts b/src/test/javascript/spec/component/course/course-management-tab-bar.component.spec.ts index 23b57fb1f705..dd17e48b8f8d 100644 --- a/src/test/javascript/spec/component/course/course-management-tab-bar.component.spec.ts +++ b/src/test/javascript/spec/component/course/course-management-tab-bar.component.spec.ts @@ -15,6 +15,7 @@ import { CourseExamArchiveButtonComponent } from 'app/shared/components/course-e import { DeleteButtonDirective } from 'app/shared/delete-dialog/delete-button.directive'; import { HasAnyAuthorityDirective } from 'app/shared/auth/has-any-authority.directive'; import { FeatureToggleLinkDirective } from 'app/shared/feature-toggle/feature-toggle-link.directive'; +import { FeatureToggleHideDirective } from 'app/shared/feature-toggle/feature-toggle-hide.directive'; describe('Course Management Tab Bar Component', () => { let component: CourseManagementTabBarComponent; @@ -41,6 +42,7 @@ describe('Course Management Tab Bar Component', () => { MockDirective(DeleteButtonDirective), MockDirective(HasAnyAuthorityDirective), MockDirective(FeatureToggleLinkDirective), + MockDirective(FeatureToggleHideDirective), ], imports: [HttpClientTestingModule, RouterModule, ArtemisTestModule], providers: [ From 03c8d06754706e25ec4b3a67ef4db6d511eebf80 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Sat, 5 Aug 2023 11:08:22 +0200 Subject: [PATCH 139/215] adjust test cases --- .../artemis/lecture/LearningPathIntegrationTest.java | 10 ++++++++++ .../www1/artemis/service/FeatureToggleServiceTest.java | 8 ++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java index d7aba779da09..58df4717d4d7 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java @@ -34,6 +34,8 @@ import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.service.CompetencyProgressService; import de.tum.in.www1.artemis.service.LectureUnitService; +import de.tum.in.www1.artemis.service.feature.Feature; +import de.tum.in.www1.artemis.service.feature.FeatureToggleService; import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.util.PageableSearchUtilService; import de.tum.in.www1.artemis.web.rest.dto.competency.*; @@ -90,6 +92,9 @@ class LearningPathIntegrationTest extends AbstractSpringIntegrationBambooBitbuck @Autowired private LearningPathUtilService learningPathUtilService; + @Autowired + private FeatureToggleService featureToggleService; + private Course course; private Competency[] competencies; @@ -108,6 +113,11 @@ class LearningPathIntegrationTest extends AbstractSpringIntegrationBambooBitbuck private User studentNotInCourse; + @BeforeEach + void enableLearningPathsFeatureToggle() { + featureToggleService.enableFeature(Feature.LearningPaths); + } + @BeforeEach void setupTestScenario() throws Exception { userUtilService.addUsers(TEST_PREFIX, NUMBER_OF_STUDENTS, 1, 1, 1); diff --git a/src/test/java/de/tum/in/www1/artemis/service/FeatureToggleServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/FeatureToggleServiceTest.java index 17072ad5d1a1..6d9abb76a563 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/FeatureToggleServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/FeatureToggleServiceTest.java @@ -59,22 +59,22 @@ void testEnableDisableFeature() { @Test void testShouldNotEnableTwice() { - assertThat(featureToggleService.enabledFeatures()).hasSameSizeAs(Feature.values()); + assertThat(featureToggleService.enabledFeatures().size()).isEqualTo(Feature.values().length - 1); featureToggleService.enableFeature(Feature.ProgrammingExercises); // Feature should not be added multiple times - assertThat(featureToggleService.enabledFeatures()).hasSameSizeAs(Feature.values()); + assertThat(featureToggleService.enabledFeatures().size()).isEqualTo(Feature.values().length - 1); } @Test void testShouldNotDisableTwice() { featureToggleService.disableFeature(Feature.ProgrammingExercises); - assertThat(featureToggleService.disabledFeatures()).hasSize(1); + assertThat(featureToggleService.disabledFeatures()).hasSize(2); featureToggleService.disableFeature(Feature.ProgrammingExercises); // Feature should not be added multiple times - assertThat(featureToggleService.disabledFeatures()).hasSize(1); + assertThat(featureToggleService.disabledFeatures()).hasSize(2); // Reset featureToggleService.enableFeature(Feature.ProgrammingExercises); From 0848fc3811b3332a4fefd0a6eb6542146e896b98 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Sat, 5 Aug 2023 11:28:41 +0200 Subject: [PATCH 140/215] Update learning-path-management.component.html --- .../learning-path-management.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html index 9447ba7680ea..c6e89d1e7a8c 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html @@ -4,7 +4,7 @@
    -
    +

    Learning Path Management

    Disabled From 1a8f586d1b0b27b39b25c3aa358f1712e38ba1ba Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Sat, 5 Aug 2023 12:21:45 +0200 Subject: [PATCH 141/215] adjust feature toggle for testing --- .../www1/artemis/lecture/LearningPathIntegrationTest.java | 6 ++++++ .../in/www1/artemis/service/FeatureToggleServiceTest.java | 1 + 2 files changed, 7 insertions(+) diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java index 58df4717d4d7..35555200b01a 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java @@ -9,6 +9,7 @@ import java.util.function.Function; import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -118,6 +119,11 @@ void enableLearningPathsFeatureToggle() { featureToggleService.enableFeature(Feature.LearningPaths); } + @AfterEach + void disableLearningPathsFeatureToggle() { + featureToggleService.disableFeature(Feature.LearningPaths); + } + @BeforeEach void setupTestScenario() throws Exception { userUtilService.addUsers(TEST_PREFIX, NUMBER_OF_STUDENTS, 1, 1, 1); diff --git a/src/test/java/de/tum/in/www1/artemis/service/FeatureToggleServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/FeatureToggleServiceTest.java index 6d9abb76a563..758b8fc2e204 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/FeatureToggleServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/FeatureToggleServiceTest.java @@ -23,6 +23,7 @@ void checkReset() { // Verify that the test has reset the state // Must be extended if additional features are added assertThat(featureToggleService.isFeatureEnabled(Feature.ProgrammingExercises)).isTrue(); + assertThat(featureToggleService.isFeatureEnabled(Feature.LearningPaths)).isFalse(); } @Test From 8107b727aaa69c113070db41c98ef8ecd475dc6e Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Mon, 7 Aug 2023 12:32:28 +0200 Subject: [PATCH 142/215] improve competency health check and warnings --- .../CompetencyRelationRepository.java | 6 ++ .../repository/CompetencyRepository.java | 3 + .../artemis/service/LearningPathService.java | 42 ++++++++++-- .../dto/competency/LearningPathHealthDTO.java | 10 +-- ...-path-health-status-warning.component.html | 9 +++ ...ng-path-health-status-warning.component.ts | 16 +++++ .../learning-path-management.component.html | 32 +++++---- .../learning-path-management.component.ts | 15 +++- .../learning-paths/learning-paths.module.ts | 2 + .../competency/learning-path-health.model.ts | 51 +++++++++++++- src/main/webapp/i18n/de/competency.json | 12 ++++ src/main/webapp/i18n/en/competency.json | 12 ++++ .../service/LearningPathServiceTest.java | 32 ++++++++- ...th-health-status-warning.component.spec.ts | 68 +++++++++++++++++++ 14 files changed, 280 insertions(+), 30 deletions(-) create mode 100644 src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-health-status-warning.component.html create mode 100644 src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-health-status-warning.component.ts create mode 100644 src/test/javascript/spec/component/learning-paths/management/learning-path-health-status-warning.component.spec.ts diff --git a/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRelationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRelationRepository.java index bea1a8864da1..57a60304e674 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRelationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRelationRepository.java @@ -33,4 +33,10 @@ public interface CompetencyRelationRepository extends JpaRepository findAllByCourseId(@Param("courseId") Long courseId); + @Query(""" + SELECT count(cr) + FROM CompetencyRelation cr + WHERE cr.headCompetency.course.id = :courseId OR cr.tailCompetency.course.id = :courseId + """) + long countByCourseId(@Param("courseId") long courseId); } diff --git a/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRepository.java index 18be4967ccf2..cfffc608f4fd 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRepository.java @@ -11,6 +11,7 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; @@ -180,4 +181,6 @@ default Competency findByIdWithExercisesAndLectureUnitsElseThrow(Long competency default Competency findByIdWithExercisesElseThrow(Long competencyId) { return findByIdWithExercises(competencyId).orElseThrow(() -> new EntityNotFoundException("Competency", competencyId)); } + + long countByCourse(Course course); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index 64f2bfff35cb..f5972e58d9ba 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -49,14 +49,17 @@ public class LearningPathService { private final CourseRepository courseRepository; + private final CompetencyRepository competencyRepository; + private final CompetencyRelationRepository competencyRelationRepository; public LearningPathService(UserRepository userRepository, LearningPathRepository learningPathRepository, CompetencyProgressRepository competencyProgressRepository, - CourseRepository courseRepository, CompetencyRelationRepository competencyRelationRepository) { + CourseRepository courseRepository, CompetencyRepository competencyRepository, CompetencyRelationRepository competencyRelationRepository) { this.userRepository = userRepository; this.learningPathRepository = learningPathRepository; this.competencyProgressRepository = competencyProgressRepository; this.courseRepository = courseRepository; + this.competencyRepository = competencyRepository; this.competencyRelationRepository = competencyRelationRepository; } @@ -178,17 +181,46 @@ private void updateLearningPathProgress(@NotNull LearningPath learningPath) { */ public LearningPathHealthDTO getHealthStatusForCourse(@NotNull Course course) { if (!course.getLearningPathsEnabled()) { - return new LearningPathHealthDTO(LearningPathHealthDTO.HealthStatus.DISABLED); + return new LearningPathHealthDTO(Set.of(LearningPathHealthDTO.HealthStatus.DISABLED)); + } + + Set status = new HashSet<>(); + Long numberOfMissingLearningPaths = checkMissingLearningPaths(course, status); + checkNoCompetencies(course, status); + checkNoRelations(course, status); + + // if no issues where found, add OK status + if (status.isEmpty()) { + status.add(LearningPathHealthDTO.HealthStatus.OK); } + return new LearningPathHealthDTO(status, numberOfMissingLearningPaths); + } + + private Long checkMissingLearningPaths(@NotNull Course course, @NotNull Set status) { long numberOfStudents = userRepository.countUserInGroup(course.getStudentGroupName()); long numberOfLearningPaths = learningPathRepository.countLearningPathsOfEnrolledStudentsInCourse(course.getId()); + Long numberOfMissingLearningPaths = numberOfStudents - numberOfLearningPaths; - if (numberOfStudents == numberOfLearningPaths) { - return new LearningPathHealthDTO(LearningPathHealthDTO.HealthStatus.OK); + if (numberOfMissingLearningPaths != 0) { + status.add(LearningPathHealthDTO.HealthStatus.MISSING); } else { - return new LearningPathHealthDTO(LearningPathHealthDTO.HealthStatus.MISSING, numberOfStudents - numberOfLearningPaths); + numberOfMissingLearningPaths = null; + } + + return numberOfMissingLearningPaths; + } + + private void checkNoCompetencies(@NotNull Course course, @NotNull Set status) { + if (competencyRepository.countByCourse(course) == 0) { + status.add(LearningPathHealthDTO.HealthStatus.NO_COMPETENCIES); + } + } + + private void checkNoRelations(@NotNull Course course, @NotNull Set status) { + if (competencyRelationRepository.countByCourseId(course.getId()) == 0) { + status.add(LearningPathHealthDTO.HealthStatus.NO_RELATIONS); } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/LearningPathHealthDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/LearningPathHealthDTO.java index f261bf7cb26a..cea77a15d594 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/LearningPathHealthDTO.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/LearningPathHealthDTO.java @@ -1,17 +1,19 @@ package de.tum.in.www1.artemis.web.rest.dto.competency; -import javax.validation.constraints.NotNull; +import java.util.Set; + +import javax.validation.constraints.NotEmpty; import com.fasterxml.jackson.annotation.JsonInclude; @JsonInclude(JsonInclude.Include.NON_NULL) -public record LearningPathHealthDTO(@NotNull HealthStatus status, Long missingLearningPaths) { +public record LearningPathHealthDTO(@NotEmpty Set status, Long missingLearningPaths) { - public LearningPathHealthDTO(HealthStatus status) { + public LearningPathHealthDTO(Set status) { this(status, null); } public enum HealthStatus { - OK, DISABLED, MISSING + OK, DISABLED, MISSING, NO_COMPETENCIES, NO_RELATIONS } } diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-health-status-warning.component.html b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-health-status-warning.component.html new file mode 100644 index 000000000000..5b2829f66531 --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-health-status-warning.component.html @@ -0,0 +1,9 @@ +
    +
    +
    {{ getWarningTitle(status) | artemisTranslate }}
    +

    {{ getWarningBody(status) | artemisTranslate }}

    + +
    +
    diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-health-status-warning.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-health-status-warning.component.ts new file mode 100644 index 000000000000..fe9cc0d7adcf --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-health-status-warning.component.ts @@ -0,0 +1,16 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { HealthStatus, getWarningAction, getWarningBody, getWarningHint, getWarningTitle } from 'app/entities/competency/learning-path-health.model'; + +@Component({ + selector: 'jhi-learning-path-health-status-warning', + templateUrl: './learning-path-health-status-warning.component.html', +}) +export class LearningPathHealthStatusWarningComponent { + @Input() status: HealthStatus; + @Output() onButtonClicked: EventEmitter = new EventEmitter(); + + protected readonly getWarningTitle = getWarningTitle; + protected readonly getWarningBody = getWarningBody; + protected readonly getWarningHint = getWarningHint; + protected readonly getWarningAction = getWarningAction; +} diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html index c6e89d1e7a8c..5542ff44ebf6 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html @@ -6,7 +6,7 @@

    Learning Path Management

    -
    +
    Disabled
    -
    -
    -
    {{ 'artemisApp.learningPath.manageLearningPaths.health.missing.title' | artemisTranslate }}
    -

    {{ 'artemisApp.learningPath.manageLearningPaths.health.missing.body' | artemisTranslate }}

    - -
    -
    -
    + + + + + +
    Search for Learning Path: diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts index f8cafc4a67f0..ca4f758813f4 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { Subject } from 'rxjs'; import { LearningPathService } from 'app/course/learning-paths/learning-path.service'; import { debounceTime, finalize, switchMap, tap } from 'rxjs/operators'; @@ -11,7 +11,7 @@ import { LearningPathPagingService } from 'app/course/learning-paths/learning-pa import { SortService } from 'app/shared/service/sort.service'; import { LearningPathPageableSearchDTO } from 'app/entities/competency/learning-path.model'; import { faSort, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons'; -import { HealthStatus, LearningPathHealthDTO } from 'app/entities/competency/learning-path-health.model'; +import { HealthStatus, LearningPathHealthDTO, getWarningAction, getWarningBody, getWarningHint, getWarningTitle } from 'app/entities/competency/learning-path-health.model'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { LearningPathProgressModalComponent } from 'app/course/learning-paths/learning-path-management/learning-path-progress-modal.component'; @@ -53,6 +53,7 @@ export class LearningPathManagementComponent implements OnInit { constructor( private activatedRoute: ActivatedRoute, + private router: Router, private learningPathService: LearningPathService, private alertService: AlertService, private pagingService: LearningPathPagingService, @@ -134,7 +135,7 @@ export class LearningPathManagementComponent implements OnInit { .subscribe({ next: (res) => { this.health = res.body!; - if (this.health.status !== HealthStatus.DISABLED) { + if (!this.health.status?.includes(HealthStatus.DISABLED)) { this.performSearch(this.sort, 0); this.performSearch(this.search, 300); } @@ -163,6 +164,10 @@ export class LearningPathManagementComponent implements OnInit { }); } + routeToCompetencyManagement() { + this.router.navigate(['../competency-management'], { relativeTo: this.activatedRoute }); + } + /** * Method to perform the search based on a search subject * @@ -213,4 +218,8 @@ export class LearningPathManagementComponent implements OnInit { } protected readonly HealthStatus = HealthStatus; + protected readonly getWarningTitle = getWarningTitle; + protected readonly getWarningBody = getWarningBody; + protected readonly getWarningAction = getWarningAction; + protected readonly getWarningHint = getWarningHint; } diff --git a/src/main/webapp/app/course/learning-paths/learning-paths.module.ts b/src/main/webapp/app/course/learning-paths/learning-paths.module.ts index 8c37905813e9..4f5ba28c7ec7 100644 --- a/src/main/webapp/app/course/learning-paths/learning-paths.module.ts +++ b/src/main/webapp/app/course/learning-paths/learning-paths.module.ts @@ -13,11 +13,13 @@ import { LearningPathGraphNodeComponent } from 'app/course/learning-paths/learni import { CompetencyNodeDetailsComponent } from 'app/course/learning-paths/learning-path-graph/node-details/competency-node-details.component'; import { LectureUnitNodeDetailsComponent } from 'app/course/learning-paths/learning-path-graph/node-details/lecture-unit-node-details.component'; import { ExerciseNodeDetailsComponent } from 'app/course/learning-paths/learning-path-graph/node-details/exercise-node-details.component'; +import { LearningPathHealthStatusWarningComponent } from 'app/course/learning-paths/learning-path-management/learning-path-health-status-warning.component'; @NgModule({ imports: [ArtemisSharedModule, FormsModule, ReactiveFormsModule, ArtemisSharedComponentModule, NgxGraphModule, ArtemisLectureUnitsModule, ArtemisCompetenciesModule], declarations: [ LearningPathManagementComponent, + LearningPathHealthStatusWarningComponent, LearningPathProgressModalComponent, LearningPathProgressNavComponent, LearningPathGraphComponent, diff --git a/src/main/webapp/app/entities/competency/learning-path-health.model.ts b/src/main/webapp/app/entities/competency/learning-path-health.model.ts index bf3a9794d178..1cbcb13ba367 100644 --- a/src/main/webapp/app/entities/competency/learning-path-health.model.ts +++ b/src/main/webapp/app/entities/competency/learning-path-health.model.ts @@ -1,8 +1,8 @@ export class LearningPathHealthDTO { - public status?: HealthStatus; + public status?: HealthStatus[]; public missingLearningPaths?: number; - constructor(status: HealthStatus) { + constructor(status: HealthStatus[]) { this.status = status; } } @@ -11,4 +11,51 @@ export enum HealthStatus { OK = 'OK', DISABLED = 'DISABLED', MISSING = 'MISSING', + NO_COMPETENCIES = 'NO_COMPETENCIES', + NO_RELATIONS = 'NO_RELATIONS', +} + +function getWarningTranslation(status: HealthStatus, element: string) { + if (!status || status === HealthStatus.OK || status === HealthStatus.DISABLED) { + return ''; + } + + const translation = { + [HealthStatus.MISSING]: 'missing', + [HealthStatus.NO_COMPETENCIES]: 'noCompetencies', + [HealthStatus.NO_RELATIONS]: 'noRelations', + }; + return `artemisApp.learningPath.manageLearningPaths.health.${translation[status]}.${element}`; +} + +export function getWarningTitle(status: HealthStatus) { + if (!status || status === HealthStatus.OK || status === HealthStatus.DISABLED) { + return ''; + } + + return getWarningTranslation(status, 'title'); +} + +export function getWarningBody(status: HealthStatus) { + if (!status || status === HealthStatus.OK || status === HealthStatus.DISABLED) { + return ''; + } + + return getWarningTranslation(status, 'body'); +} + +export function getWarningAction(status: HealthStatus) { + if (!status || status === HealthStatus.OK || status === HealthStatus.DISABLED) { + return ''; + } + + return getWarningTranslation(status, 'action'); +} + +export function getWarningHint(status: HealthStatus) { + if (!status || status === HealthStatus.OK || status === HealthStatus.DISABLED) { + return ''; + } + + return getWarningTranslation(status, 'hint'); } diff --git a/src/main/webapp/i18n/de/competency.json b/src/main/webapp/i18n/de/competency.json index 6543d4a798a8..fdbad726d235 100644 --- a/src/main/webapp/i18n/de/competency.json +++ b/src/main/webapp/i18n/de/competency.json @@ -159,6 +159,18 @@ "body": "Für einige Studierende wurde noch kein Lernpfad erstellt. Dies ist nicht kritisch. Ihre Lernpfade werden generiert, wenn sie ihren Lernpfad das erste mal anfragen.", "action": "Erstellen", "hint": "Erstellen der fehlenden Lernpfade" + }, + "noCompetencies": { + "title": "Keine Kompetenzen", + "body": "Es wurden noch keine Kompetenzen erstellt. Lernpfade setzen sich aus den Kompetenzen zusammen, die Studierende erreichen müssen. Gehe zum Kompetenzmanagement um neue Kompetenzen zu erstellen oder bestehende zu importieren.", + "action": "Kompetenzmanagement", + "hint": "Gehe zum Kompetenzmanagement" + }, + "noRelations": { + "title": "Keine Beziehungen", + "body": "Es wurden noch keine Beziehungen zwischen Kompetenzen konfiguriert. Lernpfade nutzen die Informationen über die Struktur der Lerninhalte um zuverlässig qualitiv hochwertige Empfehlungen für Studierende zu generieren. Gehe zum Kompetenzmanagement um die Beziehungen zwischen Kompetenzen zu konfigurieren.", + "action": "Kompetenzmanagement", + "hint": "Gehe zum Kompetenzmanagement" } }, "isDisabled": "Lernpfade sind für diesen Kurs nicht aktiviert.", diff --git a/src/main/webapp/i18n/en/competency.json b/src/main/webapp/i18n/en/competency.json index ba6dc5f20b2e..8a1d3eb4a393 100644 --- a/src/main/webapp/i18n/en/competency.json +++ b/src/main/webapp/i18n/en/competency.json @@ -158,6 +158,18 @@ "body": "Some students have not generated their learning paths yet. This is not critical. Their learning paths will be created once they request their learning path for the first time.", "action": "Generate", "hint": "Generate missing Learning Paths" + }, + "noCompetencies": { + "title": "No Competencies", + "body": "You have not created competencies yet. Learning paths are composed of the competencies students have to fulfill. Go to the competency management tab to create new competencies or import existing ones.", + "action": "Competency Management", + "hint": "Go to competency management" + }, + "noRelations": { + "title": "No Relations", + "body": "You have not configured relations between competencies. Learning paths use this information about the structure of the learning materials to reliably provide high quality recommendations to students. Go to the competency management tab to configure competency relations.", + "action": "Competency Management", + "hint": "Go to competency management" } }, "isDisabled": "Learning Paths are currently disabled for this course.", diff --git a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java index 2b2e865fc42e..87ace5375578 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java @@ -88,24 +88,50 @@ void setup() { @Test void testHealthStatusDisabled() { var healthStatus = learningPathService.getHealthStatusForCourse(course); - assertThat(healthStatus.status()).isEqualTo(LearningPathHealthDTO.HealthStatus.DISABLED); + assertThat(healthStatus.status()).containsExactly(LearningPathHealthDTO.HealthStatus.DISABLED); + assertThat(healthStatus.missingLearningPaths()).isNull(); } @Test void testHealthStatusOK() { + final var competency1 = competencyUtilService.createCompetency(course); + final var competency2 = competencyUtilService.createCompetency(course); + competencyUtilService.addRelation(competency1, CompetencyRelation.RelationType.MATCHES, competency2); course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); var healthStatus = learningPathService.getHealthStatusForCourse(course); - assertThat(healthStatus.status()).isEqualTo(LearningPathHealthDTO.HealthStatus.OK); + assertThat(healthStatus.status()).containsExactly(LearningPathHealthDTO.HealthStatus.OK); + assertThat(healthStatus.missingLearningPaths()).isNull(); } @Test void testHealthStatusMissing() { + final var competency1 = competencyUtilService.createCompetency(course); + final var competency2 = competencyUtilService.createCompetency(course); + competencyUtilService.addRelation(competency1, CompetencyRelation.RelationType.MATCHES, competency2); course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); userUtilService.addStudent(TEST_PREFIX + "tumuser", TEST_PREFIX + "student1337"); var healthStatus = learningPathService.getHealthStatusForCourse(course); - assertThat(healthStatus.status()).isEqualTo(LearningPathHealthDTO.HealthStatus.MISSING); + assertThat(healthStatus.status()).containsExactly(LearningPathHealthDTO.HealthStatus.MISSING); assertThat(healthStatus.missingLearningPaths()).isEqualTo(1); } + + @Test + void testHealthStatusNoCompetencies() { + course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); + var healthStatus = learningPathService.getHealthStatusForCourse(course); + assertThat(healthStatus.status()).containsExactlyInAnyOrder(LearningPathHealthDTO.HealthStatus.NO_COMPETENCIES, LearningPathHealthDTO.HealthStatus.NO_RELATIONS); + assertThat(healthStatus.missingLearningPaths()).isNull(); + } + + @Test + void testHealthStatusNoRelations() { + competencyUtilService.createCompetency(course); + competencyUtilService.createCompetency(course); + course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); + var healthStatus = learningPathService.getHealthStatusForCourse(course); + assertThat(healthStatus.status()).containsExactly(LearningPathHealthDTO.HealthStatus.NO_RELATIONS); + assertThat(healthStatus.missingLearningPaths()).isNull(); + } } @Nested diff --git a/src/test/javascript/spec/component/learning-paths/management/learning-path-health-status-warning.component.spec.ts b/src/test/javascript/spec/component/learning-paths/management/learning-path-health-status-warning.component.spec.ts new file mode 100644 index 000000000000..f5396e32157d --- /dev/null +++ b/src/test/javascript/spec/component/learning-paths/management/learning-path-health-status-warning.component.spec.ts @@ -0,0 +1,68 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ArtemisTestModule } from '../../../test.module'; +import { MockPipe } from 'ng-mocks'; +import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { NgbTooltipMocksModule } from '../../../helpers/mocks/directive/ngbTooltipMocks.module'; +import { LearningPathHealthStatusWarningComponent } from 'app/course/learning-paths/learning-path-management/learning-path-health-status-warning.component'; +import { HealthStatus } from 'app/entities/competency/learning-path-health.model'; +import { MockHasAnyAuthorityDirective } from '../../../helpers/mocks/directive/mock-has-any-authority.directive'; + +describe('LearningPathHealthStatusWarningComponent', () => { + let fixture: ComponentFixture; + let comp: LearningPathHealthStatusWarningComponent; + let getWarningTitleStub: jest.SpyInstance; + let getWarningBodyStub: jest.SpyInstance; + let getWarningActionStub: jest.SpyInstance; + let getWarningHintStub: jest.SpyInstance; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule, NgbTooltipMocksModule], + declarations: [LearningPathHealthStatusWarningComponent, MockPipe(ArtemisTranslatePipe), MockHasAnyAuthorityDirective], + providers: [], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(LearningPathHealthStatusWarningComponent); + comp = fixture.componentInstance; + getWarningTitleStub = jest.spyOn(comp, 'getWarningTitle'); + getWarningBodyStub = jest.spyOn(comp, 'getWarningBody'); + getWarningActionStub = jest.spyOn(comp, 'getWarningAction'); + getWarningHintStub = jest.spyOn(comp, 'getWarningHint'); + fixture.detectChanges(); + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should create', () => { + expect(fixture).toBeTruthy(); + expect(comp).toBeTruthy(); + }); + + it.each([HealthStatus.MISSING, HealthStatus.NO_COMPETENCIES, HealthStatus.NO_RELATIONS])('should load title', (status: HealthStatus) => { + comp.status = status; + fixture.detectChanges(); + expect(getWarningTitleStub).toHaveBeenCalledWith(status); + }); + + it.each([HealthStatus.MISSING, HealthStatus.NO_COMPETENCIES, HealthStatus.NO_RELATIONS])('should load body', (status: HealthStatus) => { + comp.status = status; + fixture.detectChanges(); + expect(getWarningBodyStub).toHaveBeenCalledWith(status); + }); + + it.each([HealthStatus.MISSING, HealthStatus.NO_COMPETENCIES, HealthStatus.NO_RELATIONS])('should load action', (status: HealthStatus) => { + comp.status = status; + fixture.detectChanges(); + expect(getWarningActionStub).toHaveBeenCalledWith(status); + }); + + it.each([HealthStatus.MISSING, HealthStatus.NO_COMPETENCIES, HealthStatus.NO_RELATIONS])('should load hint', (status: HealthStatus) => { + comp.status = status; + fixture.detectChanges(); + expect(getWarningHintStub).toHaveBeenCalledWith(status); + }); +}); From ec1fd79e4e00c50c0694890d6534ddbaf7876e95 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Tue, 8 Aug 2023 11:55:42 +0200 Subject: [PATCH 143/215] Adjust testGenerateLearningPathOnEnrollment for changed return type of enrollement endpoint --- .../tum/in/www1/artemis/repository/UserRepository.java | 3 +++ .../artemis/lecture/LearningPathIntegrationTest.java | 9 +++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/UserRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/UserRepository.java index 4f9cd9a77b77..6fc5ed000500 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/UserRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/UserRepository.java @@ -97,6 +97,9 @@ public interface UserRepository extends JpaRepository, JpaSpecificat @EntityGraph(type = LOAD, attributePaths = { "groups", "authorities", "guidedTourSettings" }) Optional findOneWithGroupsAuthoritiesAndGuidedTourSettingsByLogin(String login); + @EntityGraph(type = LOAD, attributePaths = { "learningPaths" }) + Optional findOneWithLearningPathsByLogin(String login); + @EntityGraph(type = LOAD, attributePaths = { "learningPaths" }) Optional findWithLearningPathsById(long userId); diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java index 35555200b01a..e478eb07629c 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java @@ -276,10 +276,11 @@ void testGenerateLearningPathOnEnrollment() throws Exception { this.setupEnrollmentRequestMocks(); - final var updatedUser = request.postWithResponseBody("/api/courses/" + course.getId() + "/enroll", null, User.class, HttpStatus.OK); - final var updatedUserWithLearningPaths = userRepository.findWithLearningPathsByIdElseThrow(updatedUser.getId()); - assertThat(updatedUserWithLearningPaths.getLearningPaths()).isNotNull(); - assertThat(updatedUserWithLearningPaths.getLearningPaths().size()).as("should create LearningPath for student").isEqualTo(1); + request.postWithResponseBody("/api/courses/" + course.getId() + "/enroll", null, Set.class, HttpStatus.OK); + final var user = userRepository.findOneWithLearningPathsByLogin(TEST_PREFIX + "student1337").orElseThrow(); + + assertThat(user.getLearningPaths()).isNotNull(); + assertThat(user.getLearningPaths().size()).as("should create LearningPath for student").isEqualTo(1); } private void setupEnrollmentRequestMocks() throws JsonProcessingException, URISyntaxException { From 592adb2c716feb862f7dcacbc6a1c060a220aaf1 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Tue, 8 Aug 2023 12:00:24 +0200 Subject: [PATCH 144/215] remove clusters --- .../artemis/service/LearningPathService.java | 36 ++++++++----------- .../web/rest/LearningPathResource.java | 8 ++--- .../dto/competency/NgxLearningPathDTO.java | 6 +--- .../learning-path-graph.component.html | 1 - .../learning-path-graph.component.ts | 2 +- .../learning-paths/learning-path.service.ts | 7 ++-- .../competency/learning-path.model.ts | 6 ---- .../lecture/LearningPathIntegrationTest.java | 16 ++++----- .../service/LearningPathServiceTest.java | 29 +++++---------- .../learning-path-graph.component.spec.ts | 8 ++--- .../service/learning-path.service.spec.ts | 4 +-- 11 files changed, 44 insertions(+), 79 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index 64f2bfff35cb..4dc3b0fd2361 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -193,23 +193,22 @@ public LearningPathHealthDTO getHealthStatusForCourse(@NotNull Course course) { } /** - * Generates Ngx representation of the learning path. + * Generates Ngx graph representation of the learning path graph. * * @param learningPath the learning path for which the Ngx representation should be created - * @return Ngx representation of the learning path + * @return Ngx graph representation of the learning path * @see NgxLearningPathDTO */ - public NgxLearningPathDTO generateNgxRepresentation(@NotNull LearningPath learningPath) { + public NgxLearningPathDTO generateNgxGraphRepresentation(@NotNull LearningPath learningPath) { Set nodes = new HashSet<>(); Set edges = new HashSet<>(); - Set clusters = new HashSet<>(); - learningPath.getCompetencies().forEach(competency -> generateNgxRepresentationForCompetency(learningPath, competency, nodes, edges, clusters)); - generateNgxRepresentationForRelations(learningPath, nodes, edges); - return new NgxLearningPathDTO(nodes, edges, clusters); + learningPath.getCompetencies().forEach(competency -> generateNgxGraphRepresentationForCompetency(learningPath, competency, nodes, edges)); + generateNgxGraphRepresentationForRelations(learningPath, nodes, edges); + return new NgxLearningPathDTO(nodes, edges); } /** - * Generates Ngx representation for competency. + * Generates Ngx graph representation for competency. *

    * A competency's representation consists of *

      @@ -218,17 +217,15 @@ public NgxLearningPathDTO generateNgxRepresentation(@NotNull LearningPath learni *
    • a node for each learning unit (exercises or lecture unit)
    • *
    • edges from start node to each learning unit
    • *
    • edges from each learning unit to end node
    • - *
    • a cluster consisting of all created nodes
    • *
    * * @param learningPath the learning path for which the representation should be created * @param competency the competency for which the representation will be created * @param nodes set of nodes to store the new nodes * @param edges set of edges to store the new edges - * @param clusters set of clusters to store the new clusters */ - private void generateNgxRepresentationForCompetency(LearningPath learningPath, Competency competency, Set nodes, Set edges, - Set clusters) { + private void generateNgxGraphRepresentationForCompetency(LearningPath learningPath, Competency competency, Set nodes, + Set edges) { Set currentCluster = new HashSet<>(); // generates start and end node final var startNodeId = getCompetencyStartNodeId(competency.getId()); @@ -256,17 +253,12 @@ private void generateNgxRepresentationForCompetency(LearningPath learningPath, C if (currentCluster.size() == 2) { edges.add(new NgxLearningPathDTO.Edge(getDirectEdgeId(competency.getId()), startNodeId, endNodeId)); } - // generate cluster for competency - var childNodeIds = currentCluster.stream().map(NgxLearningPathDTO.Node::id).collect(Collectors.toSet()); - childNodeIds.add(startNodeId); - childNodeIds.add(endNodeId); - clusters.add(new NgxLearningPathDTO.Cluster(String.valueOf(competency.getId()), competency.getTitle(), childNodeIds)); nodes.addAll(currentCluster); } /** - * Generates Ngx representations for competency relations. + * Generates Ngx graph representations for competency relations. *

    * The representation will contain: *

    - - diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts index ea3d13095a58..e00c2c96d14d 100644 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts @@ -66,16 +66,7 @@ export class LearningPathContainerComponent implements OnInit { this.learningPathHistoryStorageService.storeExercise(this.learningPathId, this.exercise.id); } this.undefineAll(); - /*this.learningPathService.getRecommendation(this.learningPathId).subscribe((recommendationResponse) => { - const recommendation = recommendationResponse.body!; - this.learningObjectId = recommendation.learningObjectId; - this.lectureId = recommendation.lectureId; - if (recommendation.type == RecommendationType.LECTURE_UNIT) { - this.loadLectureUnit(); - } else if (recommendation.type === RecommendationType.EXERCISE) { - this.loadExercise(); - } - });*/ + // todo: load recommendation, part of next pr } undefineAll() { diff --git a/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.ts b/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.ts index 5ad133a7b589..6972fce84d82 100644 --- a/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.ts +++ b/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.ts @@ -24,7 +24,10 @@ export class LearningPathLectureUnitViewComponent { discussionComponent?: DiscussionSectionComponent; - constructor(private lectureUnitService: LectureUnitService, private alertService: AlertService) {} + constructor( + private lectureUnitService: LectureUnitService, + private alertService: AlertService, + ) {} completeLectureUnit(event: LectureUnitCompletionEvent): void { if (this.lecture && event.lectureUnit.visibleToStudents && event.lectureUnit.completed !== event.completed) { diff --git a/src/test/javascript/spec/component/learning-paths/participate/learning-path-container.component.spec.ts b/src/test/javascript/spec/component/learning-paths/participate/learning-path-container.component.spec.ts index f388d692e5f7..400c91dc2364 100644 --- a/src/test/javascript/spec/component/learning-paths/participate/learning-path-container.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/participate/learning-path-container.component.spec.ts @@ -1,6 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MockComponent, MockModule } from 'ng-mocks'; -import { By } from '@angular/platform-browser'; import { ArtemisTestModule } from '../../../test.module'; import { of } from 'rxjs'; import { ActivatedRoute, RouterModule } from '@angular/router'; @@ -26,7 +25,6 @@ describe('LearningPathContainerComponent', () => { let learningPathService: LearningPathService; let getLearningPathIdStub: jest.SpyInstance; const learningPathId = 1337; - //let getRecommendationStub: jest.SpyInstance; let lectureService: LectureService; let lecture: Lecture; let lectureUnit: LectureUnit; @@ -65,7 +63,6 @@ describe('LearningPathContainerComponent', () => { comp = fixture.componentInstance; learningPathService = TestBed.inject(LearningPathService); getLearningPathIdStub = jest.spyOn(learningPathService, 'getLearningPathId').mockReturnValue(of(new HttpResponse({ body: learningPathId }))); - // getRecommendationStub = jest.spyOn(learningPathService, 'getRecommendation'); lectureUnit = new AttachmentUnit(); lectureUnit.id = 3; @@ -100,37 +97,6 @@ describe('LearningPathContainerComponent', () => { expect(getLearningPathIdStub).toHaveBeenCalledWith(1); }); - // eslint-disable-next-line jest/no-commented-out-tests - /*it('should request recommendation on next button click', () => { - const button = fixture.debugElement.query(By.css('.next-button')); - expect(button).not.toBeNull(); - button.nativeElement.click(); - expect(getRecommendationStub).toHaveBeenCalledWith(learningPathId); - }); - - it('should load lecture unit on recommendation', () => { - const recommendation = new LearningPathRecommendationDTO(); - recommendation.learningObjectId = lectureUnit.id!; - recommendation.lectureId = lecture.id; - recommendation.type = RecommendationType.LECTURE_UNIT; - getRecommendationStub.mockReturnValue(of(new HttpResponse({ body: recommendation }))); - comp.onNextTask(); - expect(findWithDetailsStub).toHaveBeenCalled(); - expect(findWithDetailsStub).toHaveBeenCalledWith(lecture.id); - expect(getExerciseDetailsStub).not.toHaveBeenCalled(); - }); - - it('should load exercise on recommendation', () => { - const recommendation = new LearningPathRecommendationDTO(); - recommendation.learningObjectId = exercise.id!; - recommendation.type = RecommendationType.EXERCISE; - getRecommendationStub.mockReturnValue(of(new HttpResponse({ body: recommendation }))); - comp.onNextTask(); - expect(findWithDetailsStub).not.toHaveBeenCalled(); - expect(getExerciseDetailsStub).toHaveBeenCalled(); - expect(getExerciseDetailsStub).toHaveBeenCalledWith(exercise.id); - });*/ - it('should store current lecture unit in history', () => { comp.learningObjectId = lectureUnit.id!; comp.lectureUnit = lectureUnit; From 0f6d92f3be4547411a834f5900d09f68b4a8a6c5 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Mon, 14 Aug 2023 13:58:51 +0200 Subject: [PATCH 167/215] remove unused providers in tests --- .../participate/learning-path-graph-sidebar.component.spec.ts | 1 - .../learning-path-lecture-unit-view.component.spec.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/test/javascript/spec/component/learning-paths/participate/learning-path-graph-sidebar.component.spec.ts b/src/test/javascript/spec/component/learning-paths/participate/learning-path-graph-sidebar.component.spec.ts index f3be63c89f51..4ad793aa9cff 100644 --- a/src/test/javascript/spec/component/learning-paths/participate/learning-path-graph-sidebar.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/participate/learning-path-graph-sidebar.component.spec.ts @@ -15,7 +15,6 @@ describe('LearningPathGraphSidebarComponent', () => { TestBed.configureTestingModule({ imports: [ArtemisTestModule, MockComponent(LearningPathGraphComponent), MockPipe(ArtemisTranslatePipe), NgbTooltipMocksModule], declarations: [LearningPathGraphSidebarComponent], - providers: [], }) .compileComponents() .then(() => { diff --git a/src/test/javascript/spec/component/learning-paths/participate/learning-path-lecture-unit-view.component.spec.ts b/src/test/javascript/spec/component/learning-paths/participate/learning-path-lecture-unit-view.component.spec.ts index 137578892fef..63b7b56186e5 100644 --- a/src/test/javascript/spec/component/learning-paths/participate/learning-path-lecture-unit-view.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/participate/learning-path-lecture-unit-view.component.spec.ts @@ -23,7 +23,6 @@ describe('LearningPathLectureUnitViewComponent', () => { TestBed.configureTestingModule({ imports: [ArtemisTestModule, MockModule(RouterModule), MockModule(ArtemisLectureUnitsModule)], declarations: [LearningPathLectureUnitViewComponent], - providers: [], }) .compileComponents() .then(() => { From 78d5c310eccab8cab28cbe7c3527ec4b2597650f Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Tue, 15 Aug 2023 01:53:48 +0200 Subject: [PATCH 168/215] Update learning-path-health-status-warning.component.ts --- .../learning-path-health-status-warning.component.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-health-status-warning.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-health-status-warning.component.ts index fe9cc0d7adcf..0084bf57ed9c 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-health-status-warning.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-health-status-warning.component.ts @@ -9,8 +9,8 @@ export class LearningPathHealthStatusWarningComponent { @Input() status: HealthStatus; @Output() onButtonClicked: EventEmitter = new EventEmitter(); - protected readonly getWarningTitle = getWarningTitle; - protected readonly getWarningBody = getWarningBody; - protected readonly getWarningHint = getWarningHint; - protected readonly getWarningAction = getWarningAction; + readonly getWarningTitle = getWarningTitle; + readonly getWarningBody = getWarningBody; + readonly getWarningHint = getWarningHint; + readonly getWarningAction = getWarningAction; } From 79d653534882bf01772a8741191e1449d9fc9565 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Tue, 15 Aug 2023 02:06:17 +0200 Subject: [PATCH 169/215] fix type error --- .../management/learning-path-management.component.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts b/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts index bf89f3f55e26..9da130b9851a 100644 --- a/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts @@ -90,7 +90,7 @@ describe('LearningPathManagementComponent', () => { searchForLearningPathsStub.mockReturnValue(of(searchResult)); enableLearningPathsStub.mockReturnValue(of(new HttpResponse())); generateMissingLearningPathsForCourseStub.mockReturnValue(of(new HttpResponse())); - health = new LearningPathHealthDTO(HealthStatus.OK); + health = new LearningPathHealthDTO([HealthStatus.OK]); getHealthStatusForCourseStub.mockReturnValue(of(new HttpResponse({ body: health }))); }); @@ -122,7 +122,7 @@ describe('LearningPathManagementComponent', () => { })); it('should enable learning paths and load data', fakeAsync(() => { - const healthDisabled = new LearningPathHealthDTO(HealthStatus.DISABLED); + const healthDisabled = new LearningPathHealthDTO([HealthStatus.DISABLED]); getHealthStatusForCourseStub.mockReturnValueOnce(of(new HttpResponse({ body: healthDisabled }))).mockReturnValueOnce(of(new HttpResponse({ body: health }))); fixture.detectChanges(); comp.ngOnInit(); @@ -145,7 +145,7 @@ describe('LearningPathManagementComponent', () => { })); it('should generate missing learning paths and load data', fakeAsync(() => { - const healthMissing = new LearningPathHealthDTO(HealthStatus.MISSING); + const healthMissing = new LearningPathHealthDTO([HealthStatus.MISSING]); getHealthStatusForCourseStub.mockReturnValueOnce(of(new HttpResponse({ body: healthMissing }))).mockReturnValueOnce(of(new HttpResponse({ body: health }))); fixture.detectChanges(); comp.ngOnInit(); From 1f52ef6eb190044d56832563b84780a4167c79b3 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Wed, 16 Aug 2023 11:09:31 +0200 Subject: [PATCH 170/215] fix code style --- .../de/tum/in/www1/artemis/domain/lecture/LectureUnit.java | 2 +- .../app/shared/sticky-popover/sticky-popover.directive.ts | 3 +-- .../graph/node-details/exercise-node-details.component.spec.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/domain/lecture/LectureUnit.java b/src/main/java/de/tum/in/www1/artemis/domain/lecture/LectureUnit.java index 8f1ae8624158..d53653d4f53f 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/lecture/LectureUnit.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/lecture/LectureUnit.java @@ -122,6 +122,6 @@ public Optional 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) + // Used to distinguish the type when used in a DTO, e.g., LectureUnitForLearningPathNodeDetailsDTO. public abstract String getType(); } diff --git a/src/main/webapp/app/shared/sticky-popover/sticky-popover.directive.ts b/src/main/webapp/app/shared/sticky-popover/sticky-popover.directive.ts index 4950c06f7937..d275e8002227 100644 --- a/src/main/webapp/app/shared/sticky-popover/sticky-popover.directive.ts +++ b/src/main/webapp/app/shared/sticky-popover/sticky-popover.directive.ts @@ -13,9 +13,9 @@ import { TemplateRef, ViewContainerRef, } from '@angular/core'; - import { DOCUMENT } from '@angular/common'; import { NgbPopover, NgbPopoverConfig } from '@ng-bootstrap/ng-bootstrap'; + @Directive({ selector: '[jhiStickyPopover]', }) @@ -26,7 +26,6 @@ export class StickyPopoverDirective extends NgbPopover implements OnInit, OnDest triggers: string; container: string; - ngpPopover: TemplateRef; canClosePopover: boolean; toggle(): void { diff --git a/src/test/javascript/spec/component/learning-paths/graph/node-details/exercise-node-details.component.spec.ts b/src/test/javascript/spec/component/learning-paths/graph/node-details/exercise-node-details.component.spec.ts index 07fccf09632d..7469f93f790d 100644 --- a/src/test/javascript/spec/component/learning-paths/graph/node-details/exercise-node-details.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/graph/node-details/exercise-node-details.component.spec.ts @@ -47,7 +47,7 @@ describe('ExerciseNodeDetailsComponent', () => { expect(comp.exercise).toEqual(exercise); }); - it('should load not exercise on init if already present', () => { + it('should not load exercise on init if already present', () => { comp.exercise = exercise; fixture.detectChanges(); expect(findStub).not.toHaveBeenCalled(); From ced674a2df7641c47bab3406e49dd41420bbba47 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 17 Aug 2023 12:42:35 +0200 Subject: [PATCH 171/215] refactor lecture unit view tests to be parameterized --- ...g-path-lecture-unit-view.component.spec.ts | 49 ++++--------------- 1 file changed, 10 insertions(+), 39 deletions(-) diff --git a/src/test/javascript/spec/component/learning-paths/participate/learning-path-lecture-unit-view.component.spec.ts b/src/test/javascript/spec/component/learning-paths/participate/learning-path-lecture-unit-view.component.spec.ts index 63b7b56186e5..82324e77b0bc 100644 --- a/src/test/javascript/spec/component/learning-paths/participate/learning-path-lecture-unit-view.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/participate/learning-path-lecture-unit-view.component.spec.ts @@ -39,47 +39,18 @@ describe('LearningPathLectureUnitViewComponent', () => { jest.restoreAllMocks(); }); - it('should display attachment unit correctly', () => { - const attachment = new AttachmentUnit(); - attachment.id = 3; - lecture.lectureUnits = [attachment]; - comp.lecture = lecture; - comp.lectureUnit = attachment; - fixture.detectChanges(); - const view = fixture.debugElement.nativeElement.querySelector('jhi-attachment-unit'); - expect(view).toBeTruthy(); - }); - - it('should display video unit correctly', () => { - const video = new VideoUnit(); - video.id = 3; - lecture.lectureUnits = [video]; - comp.lecture = lecture; - comp.lectureUnit = video; - fixture.detectChanges(); - const view = fixture.debugElement.nativeElement.querySelector('jhi-video-unit'); - expect(view).toBeTruthy(); - }); - - it('should display text unit correctly', () => { - const text = new TextUnit(); - text.id = 3; - lecture.lectureUnits = [text]; - comp.lecture = lecture; - comp.lectureUnit = text; - fixture.detectChanges(); - const view = fixture.debugElement.nativeElement.querySelector('jhi-text-unit'); - expect(view).toBeTruthy(); - }); - - it('should display online unit correctly', () => { - const online = new OnlineUnit(); - online.id = 3; - lecture.lectureUnits = [online]; + it.each([ + { lectureUnit: new AttachmentUnit(), selector: 'jhi-attachment-unit' }, + { lectureUnit: new VideoUnit(), selector: 'jhi-video-unit' }, + { lectureUnit: new TextUnit(), selector: 'jhi-text-unit' }, + { lectureUnit: new OnlineUnit(), selector: 'jhi-online-unit' }, + ])('should display lecture unit correctly', ({ lectureUnit, selector }) => { + lectureUnit.id = 3; + lecture.lectureUnits = [lectureUnit]; comp.lecture = lecture; - comp.lectureUnit = online; + comp.lectureUnit = lectureUnit; fixture.detectChanges(); - const view = fixture.debugElement.nativeElement.querySelector('jhi-online-unit'); + const view = fixture.debugElement.nativeElement.querySelector(selector); expect(view).toBeTruthy(); }); From 02b766abfbe420996db20d675327a5956f11648a Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 17 Aug 2023 12:45:08 +0200 Subject: [PATCH 172/215] adjust translation? --- src/main/webapp/i18n/en/competency.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/i18n/en/competency.json b/src/main/webapp/i18n/en/competency.json index 73d444187ab1..c64992bb512d 100644 --- a/src/main/webapp/i18n/en/competency.json +++ b/src/main/webapp/i18n/en/competency.json @@ -186,9 +186,9 @@ }, "participate": { "noTaskSelected": "Currently you have no lecture unit or exercise selected.", - "prev": "Previous", + "prev": "Back", "prevHint": "Return to your previous task", - "next": "Next", + "next": "Continue", "nextHint": "Go to next recommended task" } } From 295b44d3c20642d8c07dc3bfc79580bc63d63db6 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 17 Aug 2023 13:08:56 +0200 Subject: [PATCH 173/215] add comment to undefineAll --- .../participate/learning-path-container.component.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts index e00c2c96d14d..b2e4d6524898 100644 --- a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts @@ -65,6 +65,7 @@ export class LearningPathContainerComponent implements OnInit { } else if (this.exercise?.id) { this.learningPathHistoryStorageService.storeExercise(this.learningPathId, this.exercise.id); } + // reset state to avoid invalid states this.undefineAll(); // todo: load recommendation, part of next pr } @@ -76,6 +77,7 @@ export class LearningPathContainerComponent implements OnInit { } onPrevTask() { + // reset state to avoid invalid states this.undefineAll(); if (this.learningPathHistoryStorageService.hasPrevious(this.learningPathId)) { const entry = this.learningPathHistoryStorageService.getPrevious(this.learningPathId); @@ -148,6 +150,7 @@ export class LearningPathContainerComponent implements OnInit { } else if (this.exercise?.id) { this.learningPathHistoryStorageService.storeExercise(this.learningPathId, this.exercise.id); } + // reset state to avoid invalid states this.undefineAll(); this.learningObjectId = node.linkedResource!; this.lectureId = node.linkedResourceParent; From d5c99a8bb2ef0f3635e5fa514702a23b85e10350 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 17 Aug 2023 13:11:20 +0200 Subject: [PATCH 174/215] adjust translation --- src/main/webapp/i18n/en/competency.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/i18n/en/competency.json b/src/main/webapp/i18n/en/competency.json index c64992bb512d..b4d3e89115a8 100644 --- a/src/main/webapp/i18n/en/competency.json +++ b/src/main/webapp/i18n/en/competency.json @@ -187,9 +187,9 @@ "participate": { "noTaskSelected": "Currently you have no lecture unit or exercise selected.", "prev": "Back", - "prevHint": "Return to your previous task", + "prevHint": "Return to your previous lecture unit or exercise", "next": "Continue", - "nextHint": "Go to next recommended task" + "nextHint": "Go to next recommendation" } } } From 2c6683a661a7638bc9fdd2365cead953f575e1cc Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Mon, 21 Aug 2023 15:48:36 +0200 Subject: [PATCH 175/215] add server code --- .../CompetencyRelationRepository.java | 46 ++++ .../artemis/service/LearningPathService.java | 214 +++++++++++++++++- .../web/rest/LearningPathResource.java | 30 ++- .../learning-paths/learning-path.service.ts | 26 ++- .../lecture/CompetencyIntegrationTest.java | 6 + .../service/learning-path.service.spec.ts | 8 +- 6 files changed, 315 insertions(+), 15 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRelationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRelationRepository.java index bea1a8864da1..8835b80e118a 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRelationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRelationRepository.java @@ -33,4 +33,50 @@ public interface CompetencyRelationRepository extends JpaRepository findAllByCourseId(@Param("courseId") Long courseId); + @Query(""" + SELECT DISTINCT relation.headCompetency.id + FROM CompetencyRelation relation + LEFT JOIN relation.headCompetency + LEFT JOIN relation.tailCompetency + WHERE relation.tailCompetency.id IN :competencyIds + AND relation.type <> 'MATCHES' + """) + Set getPriorCompetenciesByCompetencyIds(@Param("competencyIds") Set competencyIds); + + @Query(""" + SELECT COUNT(relation) + FROM CompetencyRelation relation + LEFT JOIN relation.headCompetency + LEFT JOIN relation.tailCompetency + WHERE relation.tailCompetency.id IN :competencyTailIds + AND relation.headCompetency.id IN :competencyHeadIds + AND relation.type = :type + """) + long countRelationsOfTypeBetweenCompetencyGroups(@Param("competencyTailIds") Set competencyTailIds, @Param("type") CompetencyRelation.RelationType type, + @Param("competencyHeadIds") Set competencyHeadIds); + + @Query(value = """ + WITH RECURSIVE transitive_closure(id) AS + ( + (SELECT competency.id FROM learning_goal as competency WHERE competency.id = :competencyId) + UNION + ( + ( + SELECT relation.head_learning_goal_id + FROM learning_goal_relation as relation + JOIN transitive_closure AS tc ON relation.tail_learning_goal_id = tc.id + WHERE relation.type = 'M' + ) + UNION + ( + SELECT relation.tail_learning_goal_id + FROM learning_goal_relation as relation + JOIN transitive_closure AS tc ON relation.head_learning_goal_id = tc.id + WHERE relation.type = 'M' + ) + ) + ) + SELECT * FROM transitive_closure + """, nativeQuery = true) + Set getMatchingCompetenciesByCompetencyId(@Param("competencyId") long competencyId); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index 1f2bc56fa27d..ae88be164c2b 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -1,5 +1,7 @@ package de.tum.in.www1.artemis.service; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @@ -14,11 +16,9 @@ import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; -import de.tum.in.www1.artemis.domain.Course; -import de.tum.in.www1.artemis.domain.User; -import de.tum.in.www1.artemis.domain.competency.Competency; -import de.tum.in.www1.artemis.domain.competency.CompetencyRelation; -import de.tum.in.www1.artemis.domain.competency.LearningPath; +import de.tum.in.www1.artemis.domain.*; +import de.tum.in.www1.artemis.domain.competency.*; +import de.tum.in.www1.artemis.domain.lecture.LectureUnit; import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.web.rest.dto.PageableSearchDTO; import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; @@ -51,6 +51,19 @@ public class LearningPathService { private final CompetencyRelationRepository competencyRelationRepository; + private final static double DUE_DATE_UTILITY = 10; + + private final static double PRIOR_UTILITY = 150; + + // Important: EXTENDS_UTILITY should be smaller than ASSUMES_UTILITY to prefer extends-relation to assumes-relations. + private final static double EXTENDS_UTILITY_RATIO = 1; + + private final static double ASSUMES_UTILITY_RATIO = 2; + + private final static double EXTENDS_OR_ASSUMES_UTILITY = 100; + + private final static double MASTERY_PROGRESS_UTILITY = 1; + public LearningPathService(UserRepository userRepository, LearningPathRepository learningPathRepository, CompetencyProgressRepository competencyProgressRepository, CourseRepository courseRepository, CompetencyRelationRepository competencyRelationRepository) { this.userRepository = userRepository; @@ -357,6 +370,193 @@ private void generateNgxGraphRepresentationForRelation(CompetencyRelation relati } } + /** + * Generates Ngx path representation of the learning path. + * + * @param learningPath the learning path for which the Ngx representation should be created + * @return Ngx path representation of the learning path + * @see NgxLearningPathDTO + */ + public NgxLearningPathDTO generateNgxPathRepresentation(@NotNull LearningPath learningPath) { + Set nodes = new HashSet<>(); + Set edges = new HashSet<>(); + + var recommendedOrderOfCompetenciesById = getRecommendedOrderOfCompetencies(learningPath); + var recommendedOrderOfCompetencies = recommendedOrderOfCompetenciesById.stream() + .map(id -> learningPath.getCompetencies().stream().filter(competency -> competency.getId().equals(id)).findFirst().get()).toList(); + + // generate ngx representation of recommended competencies + // IMPORTANT generateNgxGraphRepresentationForCompetency will be replaced by future PR + recommendedOrderOfCompetencies.forEach(competency -> generateNgxGraphRepresentationForCompetency(learningPath, competency, nodes, edges)); + // generate edges between competencies + for (int i = 0; i < recommendedOrderOfCompetencies.size() - 1; i++) { + var sourceNodeId = getCompetencyEndNodeId(recommendedOrderOfCompetenciesById.get(i)); + var targetNodeId = getCompetencyStartNodeId(recommendedOrderOfCompetenciesById.get(i + 1)); + edges.add(new NgxLearningPathDTO.Edge(getRelationEdgeId(sourceNodeId, targetNodeId), sourceNodeId, targetNodeId)); + } + + return new NgxLearningPathDTO(nodes, edges); + } + + private List getRecommendedOrderOfCompetencies(LearningPath learningPath) { + HashMap> matchingClusters = getMatchingCompetencyClusters(learningPath.getCompetencies()); + HashMap> priorsCompetencies = getPriorCompetencyMapping(learningPath.getCompetencies(), matchingClusters); + HashMap extendsCompetencies = getExtendsCompetencyMapping(learningPath.getCompetencies(), matchingClusters, priorsCompetencies); + HashMap assumesCompetencies = getAssumesCompetencyMapping(learningPath.getCompetencies(), matchingClusters, priorsCompetencies); + // TODO + RecommendationState state = new RecommendationState(null, null, matchingClusters, priorsCompetencies, extendsCompetencies, assumesCompetencies); + // CompetencyProgress progress = competency.getUserProgress().stream().filter(competencyProgress -> + // competencyProgress.getUser().getId().equals(user.getId())).findFirst().orElseThrow(); + + var pendingCompetencies = getPendingCompetencies(learningPath.getCompetencies(), state); + return simulateProgression(pendingCompetencies, state); + } + + private HashMap> getMatchingCompetencyClusters(Set competencies) { + final HashMap> matchingClusters = new HashMap<>(); + for (var competency : competencies) { + if (!matchingClusters.containsKey(competency.getId())) { + final var matchingCompetencies = competencyRelationRepository.getMatchingCompetenciesByCompetencyId(competency.getId()); + // add for each in cluster to reduce database calls (once per cluster) + matchingCompetencies.forEach(id -> matchingClusters.put(id, matchingCompetencies)); + } + } + return matchingClusters; + } + + private HashMap> getPriorCompetencyMapping(Set competencies, HashMap> matchingClusters) { + HashMap> priorsMap = new HashMap<>(); + for (var competency : competencies) { + if (!priorsMap.containsKey(competency.getId())) { + final var priors = competencyRelationRepository.getPriorCompetenciesByCompetencyIds(matchingClusters.get(competency.getId())); + // add for each in cluster to reduce database calls (once per cluster) + matchingClusters.get(competency.getId()).forEach(id -> priorsMap.put(id, priors)); + } + } + return priorsMap; + } + + private HashMap getExtendsCompetencyMapping(Set competencies, HashMap> matchingClusters, HashMap> priorsCompetencies) { + return getRelationsOfTypeCompetencyMapping(competencies, matchingClusters, priorsCompetencies, CompetencyRelation.RelationType.EXTENDS); + } + + private HashMap getAssumesCompetencyMapping(Set competencies, HashMap> matchingClusters, HashMap> priorsCompetencies) { + return getRelationsOfTypeCompetencyMapping(competencies, matchingClusters, priorsCompetencies, CompetencyRelation.RelationType.ASSUMES); + } + + private HashMap getRelationsOfTypeCompetencyMapping(Set competencies, HashMap> matchingClusters, + HashMap> priorsCompetencies, CompetencyRelation.RelationType type) { + HashMap map = new HashMap<>(); + for (var competency : competencies) { + if (!map.containsKey(competency.getId())) { + long numberOfRelations = competencyRelationRepository.countRelationsOfTypeBetweenCompetencyGroups(matchingClusters.get(competency.getId()), type, + priorsCompetencies.get(competency.getId())); + // add for each in cluster to reduce database calls (once per cluster) + matchingClusters.get(competency.getId()).forEach(id -> map.put(id, numberOfRelations)); + } + } + return map; + } + + private Set getPendingCompetencies(Set competencies, RecommendationState state) { + Set pendingCompetencies = new HashSet<>(competencies); + pendingCompetencies.removeIf(competency -> state.masteredCompetencies.contains(competency.getId()) + || state.matchingClusters.get(competency.getId()).stream().anyMatch(state.masteredCompetencies::contains)); + return pendingCompetencies; + } + + private List simulateProgression(Set pendingCompetencies, RecommendationState state) { + List recommendedOrder = new ArrayList<>(); + while (!pendingCompetencies.isEmpty()) { + HashMap utilities = computeUtilities(pendingCompetencies, state); + var maxEntry = utilities.entrySet().stream().max(Comparator.comparingDouble(Map.Entry::getValue)); + // is present since outstandingCompetencies is not empty + Long competencyId = maxEntry.get().getKey(); + + // add competency to recommended order + recommendedOrder.add(competencyId); + + // simulate completion of competency + state.masteredCompetencies.add(competencyId); + pendingCompetencies.removeIf(competency -> state.masteredCompetencies.contains(competency.getId()) + || state.matchingClusters.get(competency.getId()).stream().anyMatch(state.masteredCompetencies::contains)); + } + return recommendedOrder; + } + + private HashMap computeUtilities(Set competencies, RecommendationState state) { + + HashMap utilities = new HashMap<>(); + for (var competency : competencies) { + utilities.put(competency.getId(), computeUtilityOfCompetency(competency, state)); + } + return utilities; + } + + private double computeUtilityOfCompetency(Competency competency, RecommendationState state) { + // if competency is already mastered there competency has no utility + if (state.masteredCompetencies.contains(competency.getId())) { + return 0; + } + double utility = 0; + utility += computeDueDateUtility(competency); + utility += computePriorUtility(competency, state); + utility += computeExtendsOrAssumesUtility(competency, state); + utility += computeMasteryUtility(competency, state); + return utility; + } + + private static double computeDueDateUtility(Competency competency) { + final var earliestDueDate = getEarliestDueDate(competency); + if (earliestDueDate.isEmpty()) { + return 0; + } + double timeDelta = ChronoUnit.DAYS.between(ZonedDateTime.now(), earliestDueDate.get()); + + if (timeDelta < 0) { + // deadline has passed + return (-timeDelta) * DUE_DATE_UTILITY; + } + else if (timeDelta > 0) { + // deadline not passed yet + return (1 / timeDelta) * DUE_DATE_UTILITY; + } + else { + return DUE_DATE_UTILITY; + } + } + + private static Optional getEarliestDueDate(Competency competency) { + final var lectureDueDates = competency.getLectureUnits().stream().map(LectureUnit::getLecture).map(Lecture::getEndDate); + final var exerciseDueDates = competency.getExercises().stream().map(Exercise::getDueDate); + return Stream.concat(Stream.concat(Stream.of(competency.getSoftDueDate()), lectureDueDates), exerciseDueDates).filter(Objects::nonNull).min(Comparator.naturalOrder()); + } + + private static double computePriorUtility(Competency competency, RecommendationState state) { + // return max utility if no prior competencies are present + if (state.priorCompetencies.get(competency.getId()).size() == 0) { + return PRIOR_UTILITY; + } + final double masteredPriorCompetencies = state.priorCompetencies.get(competency.getId()).stream() + .filter(id -> state.masteredCompetencies.contains(id) || state.matchingClusters.get(id).stream().anyMatch(state.masteredCompetencies::contains)).count(); + final double weight = masteredPriorCompetencies / state.priorCompetencies.get(competency.getId()).size(); + return weight * PRIOR_UTILITY; + } + + private static double computeExtendsOrAssumesUtility(Competency competency, RecommendationState state) { + final double weight = state.extendsCompetencies.get(competency.getId()) * EXTENDS_UTILITY_RATIO + state.assumesCompetencies.get(competency.getId()) * ASSUMES_UTILITY_RATIO; + // return max utility if competency does not extend or assume other competencies + if (weight == 0) { + return EXTENDS_OR_ASSUMES_UTILITY; + } + return (1 / weight) * EXTENDS_OR_ASSUMES_UTILITY; + + } + + private static double computeMasteryUtility(Competency competency, RecommendationState state) { + return state.competencyMastery.get(competency.getId()) * MASTERY_PROGRESS_UTILITY; + } + public static String getCompetencyStartNodeId(long competencyId) { return "node-" + competencyId + "-start"; } @@ -412,4 +612,8 @@ public static String getRelationEdgeId(String sourceNodeId, String targetNodeId) public static String getDirectEdgeId(long competencyId) { return "edge-" + competencyId + "-direct"; } + + private record RecommendationState(Set masteredCompetencies, HashMap competencyMastery, HashMap> matchingClusters, + HashMap> priorCompetencies, HashMap extendsCompetencies, HashMap assumesCompetencies) { + } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java index a2ffce71ee0c..f6684848d63f 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java @@ -148,6 +148,24 @@ public ResponseEntity getHealthStatusForCourse(@PathVaria @EnforceAtLeastStudent public ResponseEntity getLearningPathNgxGraph(@PathVariable Long learningPathId) { log.debug("REST request to get ngx representation of learning path with id: {}", learningPathId); + return getLearningPathNgx(learningPathId, NgxRequestType.GRAPH); + } + + /** + * GET /learning-path/:learningPathId/path : Gets the ngx representation of the learning path as a sequential path. + * + * @param learningPathId the id of the learning path that should be fetched + * @return the ResponseEntity with status 200 (OK) and with body the ngx representation of the learning path + */ + @GetMapping("/learning-path/{learningPathId}/path") + @FeatureToggle(Feature.LearningPaths) + @EnforceAtLeastStudent + public ResponseEntity getLearningPathNgxPath(@PathVariable Long learningPathId) { + log.debug("REST request to get ngx path representation of learning path with id: {}", learningPathId); + return getLearningPathNgx(learningPathId, NgxRequestType.PATH); + } + + private ResponseEntity getLearningPathNgx(@PathVariable Long learningPathId, NgxRequestType type) { LearningPath learningPath = learningPathRepository.findWithEagerCompetenciesAndLearningObjectsAndCompletedUsersByIdElseThrow(learningPathId); Course course = courseRepository.findByIdElseThrow(learningPath.getCourse().getId()); if (!course.getLearningPathsEnabled()) { @@ -162,8 +180,12 @@ public ResponseEntity getLearningPathNgxGraph(@PathVariable else if (!authorizationCheckService.isAtLeastInstructorInCourse(course, null) && !authorizationCheckService.isAdmin()) { throw new AccessForbiddenException("You are not allowed to access another users learning path."); } - NgxLearningPathDTO graph = learningPathService.generateNgxGraphRepresentation(learningPath); - return ResponseEntity.ok(graph); + + NgxLearningPathDTO ngxLearningPathDTO = switch (type) { + case GRAPH -> learningPathService.generateNgxGraphRepresentation(learningPath); + case PATH -> learningPathService.generateNgxPathRepresentation(learningPath); + }; + return ResponseEntity.ok(ngxLearningPathDTO); } /** @@ -196,4 +218,8 @@ public ResponseEntity getLearningPathId(@PathVariable Long courseId) { } return ResponseEntity.ok(learningPath.getId()); } + + private enum NgxRequestType { + GRAPH, PATH + } } diff --git a/src/main/webapp/app/course/learning-paths/learning-path.service.ts b/src/main/webapp/app/course/learning-paths/learning-path.service.ts index f0ea55d45eb2..a2009ccc3a31 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path.service.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path.service.ts @@ -26,17 +26,29 @@ export class LearningPathService { getLearningPathNgxGraph(learningPathId: number): Observable> { return this.httpClient.get(`${this.resourceURL}/learning-path/${learningPathId}/graph`, { observe: 'response' }).pipe( map((ngxLearningPathResponse) => { - if (!ngxLearningPathResponse.body!.nodes) { - ngxLearningPathResponse.body!.nodes = []; - } - if (!ngxLearningPathResponse.body!.edges) { - ngxLearningPathResponse.body!.edges = []; - } - return ngxLearningPathResponse; + return this.sanitizeNgxLearningPathResponse(ngxLearningPathResponse); }), ); } + getLearningPathNgxPath(learningPathId: number): Observable> { + return this.httpClient.get(`${this.resourceURL}/learning-path/${learningPathId}/path`, { observe: 'response' }).pipe( + map((ngxLearningPathResponse) => { + return this.sanitizeNgxLearningPathResponse(ngxLearningPathResponse); + }), + ); + } + + private sanitizeNgxLearningPathResponse(ngxLearningPathResponse: HttpResponse) { + if (!ngxLearningPathResponse.body!.nodes) { + ngxLearningPathResponse.body!.nodes = []; + } + if (!ngxLearningPathResponse.body!.edges) { + ngxLearningPathResponse.body!.edges = []; + } + return ngxLearningPathResponse; + } + getLearningPathId(courseId: number) { return this.httpClient.get(`${this.resourceURL}/courses/${courseId}/learning-path-id`, { observe: 'response' }); } diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/CompetencyIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/CompetencyIntegrationTest.java index ee3c632b2a28..e9827716b0f7 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/CompetencyIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/CompetencyIntegrationTest.java @@ -727,4 +727,10 @@ void addPrerequisite_doNotAllowCycle() throws Exception { request.postWithResponseBody("/api/courses/" + course.getId() + "/prerequisites/" + competency.getId(), competency, Competency.class, HttpStatus.CONFLICT); } + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testQueryLOL() { + var res = competencyRelationRepository.getMatchingCompetenciesByCompetencyId(competency.getId()); + assertThat(res).containsExactly(competency.getId()); + } } diff --git a/src/test/javascript/spec/service/learning-path.service.spec.ts b/src/test/javascript/spec/service/learning-path.service.spec.ts index 5dc1eec4e9ed..aaa3b43badfc 100644 --- a/src/test/javascript/spec/service/learning-path.service.spec.ts +++ b/src/test/javascript/spec/service/learning-path.service.spec.ts @@ -46,12 +46,18 @@ describe('LearningPathService', () => { expect(getStub).toHaveBeenCalledWith('api/courses/1/learning-path-health', { observe: 'response' }); }); - it('should send a request to the server to get ngx representation of learning path', () => { + it('should send a request to the server to get ngx graph representation of learning path', () => { learningPathService.getLearningPathNgxGraph(1).subscribe(); expect(getStub).toHaveBeenCalledOnce(); expect(getStub).toHaveBeenCalledWith('api/learning-path/1/graph', { observe: 'response' }); }); + it('should send a request to the server to get ngx path representation of learning path', () => { + learningPathService.getLearningPathNgxPath(1).subscribe(); + expect(getStub).toHaveBeenCalledOnce(); + expect(getStub).toHaveBeenCalledWith('api/learning-path/1/path', { observe: 'response' }); + }); + it('should send a request to the server to get learning path id of the current user in the course', () => { learningPathService.getLearningPathId(1).subscribe(); expect(getStub).toHaveBeenCalledOnce(); From 57c79a092c50fc3912b31553f31a4cd7d89d8008 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Mon, 21 Aug 2023 20:06:12 +0200 Subject: [PATCH 176/215] add client code and documentation --- .../service/CompetencyProgressService.java | 27 +++- .../artemis/service/LearningPathService.java | 152 ++++++++++++++++-- .../learning-path-graph.component.ts | 65 +++++++- ...earning-path-progress-modal.component.html | 3 +- .../learning-path-progress-modal.component.ts | 4 +- ...learning-path-graph-sidebar.component.html | 1 + .../learning-path-graph-sidebar.component.ts | 4 +- 7 files changed, 232 insertions(+), 24 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/CompetencyProgressService.java b/src/main/java/de/tum/in/www1/artemis/service/CompetencyProgressService.java index faf8c65a9471..fef6eb1b4443 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/CompetencyProgressService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/CompetencyProgressService.java @@ -269,6 +269,29 @@ else if (learningObject instanceof Exercise exercise) { throw new IllegalArgumentException("Learning object must be either LectureUnit or Exercise"); } + /** + * Calculates a user's mastery level for competency given the progress. + * + * @param competencyProgress The user's progress + * @return the + */ + public static double getMastery(@NotNull CompetencyProgress competencyProgress) { + // mastery as a weighted function of progress and confidence (consistent with client) + final double weight = 2.0 / 3.0; + return (1 - weight) * competencyProgress.getProgress() + weight * competencyProgress.getConfidence(); + } + + /** + * Calculates a user's mastery progress scaled to the mastery threshold of the corresponding competency. + * + * @param competencyProgress The user's progress + * @return the + */ + public static double getMasteryProgress(@NotNull CompetencyProgress competencyProgress) { + final double mastery = getMastery(competencyProgress); + return mastery / competencyProgress.getCompetency().getMasteryThreshold(); + } + /** * Checks if the user associated to this {@code CompetencyProgress} has mastered the associated {@code Competency}. * @@ -276,9 +299,7 @@ else if (learningObject instanceof Exercise exercise) { * @return True if the user mastered the competency, false otherwise */ public static boolean isMastered(@NotNull CompetencyProgress competencyProgress) { - // mastery as a weighted function of progress and confidence (consistent with client) - final double weight = 2.0 / 3.0; - final double mastery = (1 - weight) * competencyProgress.getProgress() + weight * competencyProgress.getConfidence(); + final double mastery = getMastery(competencyProgress); return mastery >= competencyProgress.getCompetency().getMasteryThreshold(); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java index acb4ebb8ea93..f2ce8cdca15d 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningPathService.java @@ -430,20 +430,52 @@ public NgxLearningPathDTO generateNgxPathRepresentation(@NotNull LearningPath le return new NgxLearningPathDTO(nodes, edges); } + /** + * Analyzes the current progress within the learning path and generates a recommended ordering of competencies. + * + * @param learningPath the learning path that should be analyzed + * @return the recommended ordering of competencies + */ private List getRecommendedOrderOfCompetencies(LearningPath learningPath) { + RecommendationState state = generateInitialRecommendationState(learningPath); + var pendingCompetencies = getPendingCompetencies(learningPath.getCompetencies(), state); + return simulateProgression(pendingCompetencies, state); + } + + /** + * Generates the initial state of the recommendation containing all necessary information for the prediction. + * + * @param learningPath the learning path that should be analyzed + * @return the initial RecommendationState + * @see RecommendationState + */ + private RecommendationState generateInitialRecommendationState(LearningPath learningPath) { HashMap> matchingClusters = getMatchingCompetencyClusters(learningPath.getCompetencies()); HashMap> priorsCompetencies = getPriorCompetencyMapping(learningPath.getCompetencies(), matchingClusters); HashMap extendsCompetencies = getExtendsCompetencyMapping(learningPath.getCompetencies(), matchingClusters, priorsCompetencies); HashMap assumesCompetencies = getAssumesCompetencyMapping(learningPath.getCompetencies(), matchingClusters, priorsCompetencies); - // TODO - RecommendationState state = new RecommendationState(null, null, matchingClusters, priorsCompetencies, extendsCompetencies, assumesCompetencies); - // CompetencyProgress progress = competency.getUserProgress().stream().filter(competencyProgress -> - // competencyProgress.getUser().getId().equals(user.getId())).findFirst().orElseThrow(); - - var pendingCompetencies = getPendingCompetencies(learningPath.getCompetencies(), state); - return simulateProgression(pendingCompetencies, state); + Set masteredCompetencies = new HashSet<>(); + HashMap competencyMastery = new HashMap<>(); + learningPath.getCompetencies().forEach(competency -> { + final var progress = competencyProgressRepository.findByCompetencyIdAndUserId(competency.getId(), learningPath.getUser().getId()).orElseThrow(); + if (CompetencyProgressService.isMastered(progress)) { + // add competency to mastered set if mastered + masteredCompetencies.add(competency.getId()); + } + else { + // calculate mastery progress if not completed yet + competencyMastery.put(competency.getId(), CompetencyProgressService.getMasteryProgress(progress)); + } + }); + return new RecommendationState(masteredCompetencies, competencyMastery, matchingClusters, priorsCompetencies, extendsCompetencies, assumesCompetencies); } + /** + * Gets a map from competency ids to a set of all other competency ids that are connected via matching relations (transitive closure, including the competency itself). + * + * @param competencies the competencies for which the mapping should be generated + * @return map representing the matching clusters + */ private HashMap> getMatchingCompetencyClusters(Set competencies) { final HashMap> matchingClusters = new HashMap<>(); for (var competency : competencies) { @@ -456,6 +488,13 @@ private HashMap> getMatchingCompetencyClusters(Set c return matchingClusters; } + /** + * Gets a map from competency ids to a set of all other competency ids that are connected via a non-matching relation. + * + * @param competencies the competencies for which the mapping should be generated + * @param matchingClusters the map representing the corresponding matching clusters + * @return map to retrieve prior competencies + */ private HashMap> getPriorCompetencyMapping(Set competencies, HashMap> matchingClusters) { HashMap> priorsMap = new HashMap<>(); for (var competency : competencies) { @@ -468,21 +507,46 @@ private HashMap> getPriorCompetencyMapping(Set compe return priorsMap; } - private HashMap getExtendsCompetencyMapping(Set competencies, HashMap> matchingClusters, HashMap> priorsCompetencies) { - return getRelationsOfTypeCompetencyMapping(competencies, matchingClusters, priorsCompetencies, CompetencyRelation.RelationType.EXTENDS); + /** + * Gets a map from competency ids to number of competencies that the corresponding competency extends. + * + * @param competencies the competencies for which the mapping should be generated + * @param matchingClusters the map representing the corresponding matching clusters + * @param priorCompetencies the map to retrieve corresponding prior competencies + * @return map to retrieve the number of competencies a competency extends + */ + private HashMap getExtendsCompetencyMapping(Set competencies, HashMap> matchingClusters, HashMap> priorCompetencies) { + return getRelationsOfTypeCompetencyMapping(competencies, matchingClusters, priorCompetencies, CompetencyRelation.RelationType.EXTENDS); } - private HashMap getAssumesCompetencyMapping(Set competencies, HashMap> matchingClusters, HashMap> priorsCompetencies) { - return getRelationsOfTypeCompetencyMapping(competencies, matchingClusters, priorsCompetencies, CompetencyRelation.RelationType.ASSUMES); + /** + * Gets a map from competency ids to number of competencies that the corresponding competency assumes. + * + * @param competencies the competencies for which the mapping should be generated + * @param matchingClusters the map representing the corresponding matching clusters + * @param priorCompetencies the map to retrieve corresponding prior competencies + * @return map to retrieve the number of competencies a competency assumes + */ + private HashMap getAssumesCompetencyMapping(Set competencies, HashMap> matchingClusters, HashMap> priorCompetencies) { + return getRelationsOfTypeCompetencyMapping(competencies, matchingClusters, priorCompetencies, CompetencyRelation.RelationType.ASSUMES); } + /** + * Gets a map from competency ids to number of competencies that the corresponding competency relates to with the specified type. + * + * @param competencies the competencies for which the mapping should be generated + * @param matchingClusters the map representing the corresponding matching clusters + * @param priorCompetencies the map to retrieve corresponding prior competencies + * @param type the relation type that should be counted + * @return map to retrieve the number of competencies a competency extends + */ private HashMap getRelationsOfTypeCompetencyMapping(Set competencies, HashMap> matchingClusters, - HashMap> priorsCompetencies, CompetencyRelation.RelationType type) { + HashMap> priorCompetencies, CompetencyRelation.RelationType type) { HashMap map = new HashMap<>(); for (var competency : competencies) { if (!map.containsKey(competency.getId())) { long numberOfRelations = competencyRelationRepository.countRelationsOfTypeBetweenCompetencyGroups(matchingClusters.get(competency.getId()), type, - priorsCompetencies.get(competency.getId())); + priorCompetencies.get(competency.getId())); // add for each in cluster to reduce database calls (once per cluster) matchingClusters.get(competency.getId()).forEach(id -> map.put(id, numberOfRelations)); } @@ -490,6 +554,13 @@ private HashMap getRelationsOfTypeCompetencyMapping(Set return map; } + /** + * Gets the set of competencies that are themselves not mastered and no matching competency is mastered. + * + * @param competencies the set of competencies that should be filtered + * @param state the current state of the recommendation system + * @return set of pending competencies + */ private Set getPendingCompetencies(Set competencies, RecommendationState state) { Set pendingCompetencies = new HashSet<>(competencies); pendingCompetencies.removeIf(competency -> state.masteredCompetencies.contains(competency.getId()) @@ -497,6 +568,13 @@ private Set getPendingCompetencies(Set competencies, Rec return pendingCompetencies; } + /** + * Generates a recommended ordering of competencies. + * + * @param pendingCompetencies the set of pending competencies + * @param state the current state of the recommendation system + * @return recommended ordering of competencies + */ private List simulateProgression(Set pendingCompetencies, RecommendationState state) { List recommendedOrder = new ArrayList<>(); while (!pendingCompetencies.isEmpty()) { @@ -516,8 +594,14 @@ private List simulateProgression(Set pendingCompetencies, Reco return recommendedOrder; } + /** + * Generates a mapping from competency ids to their corresponding utility in the current state. + * + * @param competencies the set of competencies for which the mapping should be generated + * @param state the current state of the recommendation system + * @return map to retrieve the utility of a competency + */ private HashMap computeUtilities(Set competencies, RecommendationState state) { - HashMap utilities = new HashMap<>(); for (var competency : competencies) { utilities.put(competency.getId(), computeUtilityOfCompetency(competency, state)); @@ -525,6 +609,13 @@ private HashMap computeUtilities(Set competencies, Rec return utilities; } + /** + * Gets the utility of a competency in the current state. + * + * @param competency the competency for which the utility should be computed + * @param state the current state of the recommendation system + * @return the utility of the given competency + */ private double computeUtilityOfCompetency(Competency competency, RecommendationState state) { // if competency is already mastered there competency has no utility if (state.masteredCompetencies.contains(competency.getId())) { @@ -538,6 +629,12 @@ private double computeUtilityOfCompetency(Competency competency, RecommendationS return utility; } + /** + * Gets the utility of the competency with respect to the earliest due date of the competency. + * + * @param competency the competency for which the utility should be computed + * @return due date utility of the competency + */ private static double computeDueDateUtility(Competency competency) { final var earliestDueDate = getEarliestDueDate(competency); if (earliestDueDate.isEmpty()) { @@ -558,12 +655,25 @@ else if (timeDelta > 0) { } } + /** + * Gets the earliest due date of any learning object attached to the competency or the competency itself. + * + * @param competency the competency for which the earliest due date should be retrieved + * @return earliest due date of the competency + */ private static Optional getEarliestDueDate(Competency competency) { final var lectureDueDates = competency.getLectureUnits().stream().map(LectureUnit::getLecture).map(Lecture::getEndDate); final var exerciseDueDates = competency.getExercises().stream().map(Exercise::getDueDate); return Stream.concat(Stream.concat(Stream.of(competency.getSoftDueDate()), lectureDueDates), exerciseDueDates).filter(Objects::nonNull).min(Comparator.naturalOrder()); } + /** + * Gets the utility of the competency with respect to prior competencies. + * + * @param competency the competency for which the utility should be computed + * @param state the current state of the recommendation system + * @return prior utility of the competency + */ private static double computePriorUtility(Competency competency, RecommendationState state) { // return max utility if no prior competencies are present if (state.priorCompetencies.get(competency.getId()).size() == 0) { @@ -575,6 +685,13 @@ private static double computePriorUtility(Competency competency, RecommendationS return weight * PRIOR_UTILITY; } + /** + * Gets the utility of the competency with respect to prior competencies that are extended or assumed by this competency. + * + * @param competency the competency for which the utility should be computed + * @param state the current state of the recommendation system + * @return extends or assumes utility of the competency + */ private static double computeExtendsOrAssumesUtility(Competency competency, RecommendationState state) { final double weight = state.extendsCompetencies.get(competency.getId()) * EXTENDS_UTILITY_RATIO + state.assumesCompetencies.get(competency.getId()) * ASSUMES_UTILITY_RATIO; // return max utility if competency does not extend or assume other competencies @@ -585,6 +702,13 @@ private static double computeExtendsOrAssumesUtility(Competency competency, Reco } + /** + * Gets the utility of the competency with respect to users mastery progress within the competency. + * + * @param competency the competency for which the utility should be computed + * @param state the current state of the recommendation system + * @return mastery utility of the competency + */ private static double computeMasteryUtility(Competency competency, RecommendationState state) { return state.competencyMastery.get(competency.getId()) * MASTERY_PROGRESS_UTILITY; } diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts index b91f8c70d8c9..a4714c5c3b71 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path-graph.component.ts @@ -6,6 +6,11 @@ import { Subject } from 'rxjs'; import { LearningPathService } from 'app/course/learning-paths/learning-path.service'; import { NgxLearningPathDTO, NgxLearningPathNode } from 'app/entities/competency/learning-path.model'; +export enum LearningPathViewMode { + GRAPH, + PATH, +} + @Component({ selector: 'jhi-learning-path-graph', styleUrls: ['./learning-path-graph.component.scss'], @@ -16,8 +21,12 @@ export class LearningPathGraphComponent implements OnInit { isLoading = false; @Input() learningPathId: number; @Input() courseId: number; + @Input() viewMode?: LearningPathViewMode; + @Output() viewModeChange = new EventEmitter(); @Output() nodeClicked: EventEmitter = new EventEmitter(); ngxLearningPath: NgxLearningPathDTO; + ngxGraph?: NgxLearningPathDTO; + ngxPath?: NgxLearningPathDTO; layout: string | Layout = 'dagreCluster'; curve = shape.curveBundle; @@ -31,18 +40,57 @@ export class LearningPathGraphComponent implements OnInit { center$: Subject = new Subject(); zoomToFit$: Subject = new Subject(); - constructor(private activatedRoute: ActivatedRoute, private learningPathService: LearningPathService) {} + constructor( + private activatedRoute: ActivatedRoute, + private learningPathService: LearningPathService, + ) {} ngOnInit() { if (this.learningPathId) { - this.loadData(); + this.loadDataIfNecessary(); + } + } + + refreshData() { + if (this.ngxGraph) { + this.loadGraphRepresentation(); + } + if (this.ngxPath) { + this.loadPathRepresentation(); + } + if (this.viewMode == LearningPathViewMode.GRAPH) { + this.ngxLearningPath = this.ngxGraph!; + } else { + this.ngxLearningPath = this.ngxPath!; + } + } + + loadDataIfNecessary() { + if (this.viewMode == LearningPathViewMode.GRAPH) { + if (!this.ngxGraph) { + this.loadGraphRepresentation(); + } + this.ngxLearningPath = this.ngxGraph!; + } else { + if (!this.ngxPath) { + this.loadPathRepresentation(); + } + this.ngxLearningPath = this.ngxPath!; } } - loadData() { + loadGraphRepresentation() { this.isLoading = true; this.learningPathService.getLearningPathNgxGraph(this.learningPathId).subscribe((ngxLearningPathResponse) => { - this.ngxLearningPath = ngxLearningPathResponse.body!; + this.ngxGraph = ngxLearningPathResponse.body!; + this.isLoading = false; + }); + } + + loadPathRepresentation() { + this.isLoading = true; + this.learningPathService.getLearningPathNgxPath(this.learningPathId).subscribe((ngxLearningPathResponse) => { + this.ngxPath = ngxLearningPathResponse.body!; this.isLoading = false; }); } @@ -57,4 +105,13 @@ export class LearningPathGraphComponent implements OnInit { this.zoomToFit$.next(true); this.center$.next(true); } + + changeViewMode() { + if (this.viewMode === LearningPathViewMode.GRAPH) { + this.viewMode = LearningPathViewMode.PATH; + } else { + this.viewMode = LearningPathViewMode.GRAPH; + } + this.loadDataIfNecessary(); + } } diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.html b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.html index 7477b90ced6c..fcdcfb6496b3 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-progress-modal.component.html @@ -2,7 +2,7 @@