From 706193383986b42694a0fc3167cc35d09491c72e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20St=C3=B6hr?= Date: Tue, 2 Jul 2024 16:34:44 +0200 Subject: [PATCH 01/11] Refactor a lot to schedule prerequisites --- .../tum/in/www1/artemis/domain/Exercise.java | 8 +- .../www1/artemis/domain/LearningObject.java | 4 +- .../artemis/domain/competency/Competency.java | 127 +---------------- .../domain/competency/CompetencyJol.java | 6 +- .../domain/competency/CompetencyProgress.java | 6 +- .../domain/competency/CompetencyRelation.java | 12 +- .../domain/competency/CourseCompetency.java | 128 ++++++++++++++++++ .../domain/competency/LearningPath.java | 12 +- .../competency/StandardizedCompetency.java | 6 +- .../artemis/domain/lecture/ExerciseUnit.java | 6 +- .../artemis/domain/lecture/LectureUnit.java | 8 +- .../CompetencyProgressRepository.java | 10 +- .../repository/CompetencyRepository.java | 45 ------ .../CourseCompetencyRepository.java | 71 +++++++++- .../artemis/repository/CourseRepository.java | 13 +- .../www1/artemis/service/CourseService.java | 2 +- .../service/LearningObjectService.java | 6 +- .../artemis/service/LectureUnitService.java | 21 +-- .../competency/CompetencyProgressService.java | 46 +++---- .../competency/CompetencyRelationService.java | 15 +- .../service/competency/CompetencyService.java | 11 +- .../competency/CourseCompetencyService.java | 58 ++++++++ .../LearningPathNavigationService.java | 6 +- .../learningpath/LearningPathNgxService.java | 14 +- .../LearningPathRecommendationService.java | 58 ++++---- .../learningpath/LearningPathService.java | 10 +- .../www1/artemis/web/rest/CourseResource.java | 6 +- .../web/rest/LearningPathResource.java | 6 +- .../{ => competency}/CompetencyResource.java | 28 ++-- .../competency/CourseCompetencyResource.java | 56 ++++++++ .../competency/CompetencyGraphNodeDTO.java | 4 +- .../dto/competency/CompetencyNameDTO.java | 4 +- .../dto/competency/CompetencyRelationDTO.java | 2 +- .../competency-management.component.html | 2 +- .../competency-management.component.ts | 13 +- .../competency-relation-graph.component.ts | 4 +- .../course/competencies/competency.service.ts | 5 +- .../competencies/course-competency.service.ts | 42 ++++++ .../create-competency.component.ts | 2 +- .../webapp/app/entities/competency.model.ts | 37 +++-- .../competency/learning-path.model.ts | 4 +- .../standardized-competency.model.ts | 4 +- .../webapp/app/entities/exercise.model.ts | 4 +- .../lecture-unit/lectureUnit.model.ts | 4 +- .../webapp/app/entities/prerequisite.model.ts | 8 +- .../competency-selection.component.ts | 45 +++--- .../competency/LearningPathUtilService.java | 10 +- .../lecture/CompetencyIntegrationTest.java | 10 +- .../artemis/lecture/LectureUtilService.java | 4 +- .../service/LearningObjectServiceTest.java | 4 +- .../AbstractTutorialGroupIntegrationTest.java | 4 +- 51 files changed, 626 insertions(+), 395 deletions(-) create mode 100644 src/main/java/de/tum/in/www1/artemis/service/competency/CourseCompetencyService.java rename src/main/java/de/tum/in/www1/artemis/web/rest/{ => competency}/CompetencyResource.java (96%) create mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/competency/CourseCompetencyResource.java create mode 100644 src/main/webapp/app/course/competencies/course-competency.service.ts 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 1a54c446d174..e066f583dc28 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 @@ -50,7 +50,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonView; -import de.tum.in.www1.artemis.domain.competency.Competency; +import de.tum.in.www1.artemis.domain.competency.CourseCompetency; 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; @@ -113,7 +113,7 @@ public abstract class Exercise extends BaseExercise implements LearningObject { @JoinTable(name = "competency_exercise", joinColumns = @JoinColumn(name = "exercise_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "competency_id", referencedColumnName = "id")) @JsonIgnoreProperties({ "exercises", "course" }) @JsonView(QuizView.Before.class) - private Set competencies = new HashSet<>(); + private Set competencies = new HashSet<>(); @ElementCollection(fetch = FetchType.LAZY) @CollectionTable(name = "exercise_categories", joinColumns = @JoinColumn(name = "exercise_id")) @@ -461,11 +461,11 @@ public void setPlagiarismDetectionConfig(PlagiarismDetectionConfig plagiarismDet // jhipster-needle-entity-add-getters-setters - JHipster will add getters and setters here, do not remove @Override - public Set getCompetencies() { + public Set getCompetencies() { return competencies; } - public void setCompetencies(Set competencies) { + public void setCompetencies(Set competencies) { this.competencies = competencies; } 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 bfc783a3c02a..60498ff3ca97 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,7 +4,7 @@ import java.util.Optional; import java.util.Set; -import de.tum.in.www1.artemis.domain.competency.Competency; +import de.tum.in.www1.artemis.domain.competency.CourseCompetency; public interface LearningObject { @@ -26,5 +26,5 @@ public interface LearningObject { Long getId(); - Set getCompetencies(); + Set getCompetencies(); } 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 500ac223b164..a92d82758fb6 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 @@ -1,143 +1,18 @@ package de.tum.in.www1.artemis.domain.competency; import java.time.ZonedDateTime; -import java.util.HashSet; -import java.util.Set; -import jakarta.persistence.CascadeType; import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToMany; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; -import jakarta.persistence.PrePersist; -import jakarta.persistence.PreUpdate; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -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; @Entity @DiscriminatorValue("C") public class Competency extends CourseCompetency { - // TODO: move properties (linkedStandardizedCompetency, exercises, lectureUnits, userProgress, learningPaths) to CourseCompetency when refactoring - @ManyToOne - @JoinColumn(name = "linked_standardized_competency_id") - @JsonIgnoreProperties({ "competencies" }) - private StandardizedCompetency linkedStandardizedCompetency; - - @ManyToMany(mappedBy = "competencies") - @JsonIgnoreProperties({ "competencies", "course" }) - private Set exercises = new HashSet<>(); - - @ManyToMany(mappedBy = "competencies") - @JsonIgnoreProperties("competencies") - private Set lectureUnits = new HashSet<>(); - - @OneToMany(mappedBy = "competency", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE, orphanRemoval = true) - @JsonIgnoreProperties({ "user", "competency" }) - private Set userProgress = new HashSet<>(); - - @ManyToMany(mappedBy = "competencies") - @JsonIgnoreProperties({ "competencies", "course" }) - private Set learningPaths = new HashSet<>(); - - @OneToMany(mappedBy = "competency", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) - private Set competencyJols = new HashSet<>(); - - public Competency() { - } - public Competency(String title, String description, ZonedDateTime softDueDate, Integer masteryThreshold, CompetencyTaxonomy taxonomy, boolean optional) { super(title, description, softDueDate, masteryThreshold, taxonomy, optional); } - public StandardizedCompetency getLinkedStandardizedCompetency() { - return linkedStandardizedCompetency; - } - - public void setLinkedStandardizedCompetency(StandardizedCompetency linkedStandardizedCompetency) { - this.linkedStandardizedCompetency = linkedStandardizedCompetency; - } - - public Set getExercises() { - return exercises; - } - - public void setExercises(Set exercises) { - this.exercises = exercises; - } - - public void addExercise(Exercise exercise) { - this.exercises.add(exercise); - exercise.getCompetencies().add(this); - } - - public Set getLectureUnits() { - return lectureUnits; - } - - public void setLectureUnits(Set lectureUnits) { - this.lectureUnits = lectureUnits; - } - - /** - * Adds the lecture unit to the competency (bidirectional) - * Note: ExerciseUnits are not accepted, should be set via the connected exercise (see {@link #addExercise(Exercise)}) - * - * @param lectureUnit The lecture unit to add - */ - public void addLectureUnit(LectureUnit lectureUnit) { - if (lectureUnit instanceof ExerciseUnit) { - // The competencies of ExerciseUnits are taken from the corresponding exercise - throw new IllegalArgumentException("ExerciseUnits can not be connected to competencies"); - } - this.lectureUnits.add(lectureUnit); - lectureUnit.getCompetencies().add(this); - } - - /** - * Removes the lecture unit from the competency (bidirectional) - * Note: ExerciseUnits are not accepted, should be set via the connected exercise - * - * @param lectureUnit The lecture unit to remove - */ - public void removeLectureUnit(LectureUnit lectureUnit) { - if (lectureUnit instanceof ExerciseUnit) { - // The competencies of ExerciseUnits are taken from the corresponding exercise - throw new IllegalArgumentException("ExerciseUnits can not be disconnected from competencies"); - } - this.lectureUnits.remove(lectureUnit); - lectureUnit.getCompetencies().remove(this); - } - - public Set getUserProgress() { - return userProgress; - } - - 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 - */ - @PrePersist - @PreUpdate - public void prePersistOrUpdate() { - this.lectureUnits.removeIf(lectureUnit -> lectureUnit instanceof ExerciseUnit); + public Competency() { } } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/competency/CompetencyJol.java b/src/main/java/de/tum/in/www1/artemis/domain/competency/CompetencyJol.java index 6c0cf61a119f..37a793cc8a8f 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/competency/CompetencyJol.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/competency/CompetencyJol.java @@ -26,7 +26,7 @@ public class CompetencyJol extends DomainObject { @ManyToOne @JoinColumn(name = "competency_id", nullable = false) - private Competency competency; + private CourseCompetency competency; @ManyToOne @JoinColumn(name = "user_id", nullable = false) @@ -44,11 +44,11 @@ public class CompetencyJol extends DomainObject { @Column(name = "competency_confidence") private double competencyConfidence; - public Competency getCompetency() { + public CourseCompetency getCompetency() { return this.competency; } - public void setCompetency(Competency competency) { + public void setCompetency(CourseCompetency competency) { this.competency = competency; } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/competency/CompetencyProgress.java b/src/main/java/de/tum/in/www1/artemis/domain/competency/CompetencyProgress.java index f6803cdeb37f..4aa40d5fa5f9 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/competency/CompetencyProgress.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/competency/CompetencyProgress.java @@ -52,7 +52,7 @@ public class CompetencyProgress implements Serializable { @ManyToOne @MapsId("competencyId") @JsonIgnore - private Competency competency; + private CourseCompetency competency; @Column(name = "progress") private Double progress; @@ -81,11 +81,11 @@ public void setUser(User user) { this.user = user; } - public Competency getCompetency() { + public CourseCompetency getCompetency() { return competency; } - public void setCompetency(Competency competency) { + public void setCompetency(CourseCompetency competency) { this.competency = competency; } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/competency/CompetencyRelation.java b/src/main/java/de/tum/in/www1/artemis/domain/competency/CompetencyRelation.java index e2fcf8a5f7c2..01ca9f79e17a 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/competency/CompetencyRelation.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/competency/CompetencyRelation.java @@ -20,29 +20,29 @@ public class CompetencyRelation extends DomainObject { @ManyToOne @JoinColumn(name = "tail_competency_id", nullable = false) - private Competency tailCompetency; + private CourseCompetency tailCompetency; @ManyToOne @JoinColumn(name = "head_competency_id", nullable = false) - private Competency headCompetency; + private CourseCompetency headCompetency; @Column(name = "type", nullable = false) @Enumerated(EnumType.ORDINAL) private RelationType type; - public Competency getTailCompetency() { + public CourseCompetency getTailCompetency() { return tailCompetency; } - public void setTailCompetency(Competency tailCompetency) { + public void setTailCompetency(CourseCompetency tailCompetency) { this.tailCompetency = tailCompetency; } - public Competency getHeadCompetency() { + public CourseCompetency getHeadCompetency() { return headCompetency; } - public void setHeadCompetency(Competency headCompetency) { + public void setHeadCompetency(CourseCompetency headCompetency) { this.headCompetency = headCompetency; } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/competency/CourseCompetency.java b/src/main/java/de/tum/in/www1/artemis/domain/competency/CourseCompetency.java index f4c08381a007..28ba820d3ac3 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/competency/CourseCompetency.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/competency/CourseCompetency.java @@ -1,15 +1,23 @@ package de.tum.in.www1.artemis.domain.competency; import java.time.ZonedDateTime; +import java.util.HashSet; +import java.util.Set; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.DiscriminatorColumn; import jakarta.persistence.DiscriminatorType; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.Inheritance; import jakarta.persistence.InheritanceType; import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToMany; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.PrePersist; +import jakarta.persistence.PreUpdate; import jakarta.persistence.Table; import org.hibernate.annotations.Cache; @@ -17,8 +25,13 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; import de.tum.in.www1.artemis.domain.Course; +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; /** * CourseCompetency is an abstract class for all competency types that are part of a course. @@ -29,6 +42,13 @@ @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "discriminator", discriminatorType = DiscriminatorType.STRING) @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +// @formatter:off +@JsonSubTypes({ + @JsonSubTypes.Type(value = Competency.class, name = "competency"), + @JsonSubTypes.Type(value = Prerequisite.class, name = "prerequisite") +}) +// @formatter:on public abstract class CourseCompetency extends BaseCompetency { @JsonIgnore @@ -46,6 +66,27 @@ public abstract class CourseCompetency extends BaseCompetency { @Column(name = "optional") private boolean optional; + @ManyToOne + @JoinColumn(name = "linked_standardized_competency_id") + @JsonIgnoreProperties({ "competencies" }) + private StandardizedCompetency linkedStandardizedCompetency; + + @ManyToMany(mappedBy = "competencies") + @JsonIgnoreProperties({ "competencies", "course" }) + private Set exercises = new HashSet<>(); + + @ManyToMany(mappedBy = "competencies") + @JsonIgnoreProperties("competencies") + private Set lectureUnits = new HashSet<>(); + + @OneToMany(mappedBy = "competency", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE, orphanRemoval = true) + @JsonIgnoreProperties({ "user", "competency" }) + private Set userProgress = new HashSet<>(); + + @ManyToMany(mappedBy = "competencies") + @JsonIgnoreProperties({ "competencies", "course" }) + private Set learningPaths = new HashSet<>(); + @ManyToOne @JoinColumn(name = "course_id") @JsonIgnoreProperties({ "competencies", "prerequisites" }) @@ -56,6 +97,9 @@ public abstract class CourseCompetency extends BaseCompetency { @JsonIgnoreProperties({ "competencies" }) private CourseCompetency linkedCourseCompetency; + @OneToMany(mappedBy = "competency", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + private Set competencyJols = new HashSet<>(); + public CourseCompetency() { } @@ -106,4 +150,88 @@ public CourseCompetency getLinkedCourseCompetency() { public void setLinkedCourseCompetency(CourseCompetency linkedCourseCompetency) { this.linkedCourseCompetency = linkedCourseCompetency; } + + public StandardizedCompetency getLinkedStandardizedCompetency() { + return linkedStandardizedCompetency; + } + + public void setLinkedStandardizedCompetency(StandardizedCompetency linkedStandardizedCompetency) { + this.linkedStandardizedCompetency = linkedStandardizedCompetency; + } + + public Set getExercises() { + return exercises; + } + + public void setExercises(Set exercises) { + this.exercises = exercises; + } + + public void addExercise(Exercise exercise) { + this.exercises.add(exercise); + exercise.getCompetencies().add(this); + } + + public Set getLectureUnits() { + return lectureUnits; + } + + public void setLectureUnits(Set lectureUnits) { + this.lectureUnits = lectureUnits; + } + + /** + * Adds the lecture unit to the competency (bidirectional) + * Note: ExerciseUnits are not accepted, should be set via the connected exercise (see {@link #addExercise(Exercise)}) + * + * @param lectureUnit The lecture unit to add + */ + public void addLectureUnit(LectureUnit lectureUnit) { + if (lectureUnit instanceof ExerciseUnit) { + // The competencies of ExerciseUnits are taken from the corresponding exercise + throw new IllegalArgumentException("ExerciseUnits can not be connected to competencies"); + } + this.lectureUnits.add(lectureUnit); + lectureUnit.getCompetencies().add(this); + } + + /** + * Removes the lecture unit from the competency (bidirectional) + * Note: ExerciseUnits are not accepted, should be set via the connected exercise + * + * @param lectureUnit The lecture unit to remove + */ + public void removeLectureUnit(LectureUnit lectureUnit) { + if (lectureUnit instanceof ExerciseUnit) { + // The competencies of ExerciseUnits are taken from the corresponding exercise + throw new IllegalArgumentException("ExerciseUnits can not be disconnected from competencies"); + } + this.lectureUnits.remove(lectureUnit); + lectureUnit.getCompetencies().remove(this); + } + + public Set getUserProgress() { + return userProgress; + } + + 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 + */ + @PrePersist + @PreUpdate + public void prePersistOrUpdate() { + this.lectureUnits.removeIf(lectureUnit -> lectureUnit instanceof ExerciseUnit); + } } 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 72099b40a5bc..daedaef9df3f 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 @@ -43,7 +43,7 @@ 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" }) - private Set competencies = new HashSet<>(); + private Set competencies = new HashSet<>(); public int getProgress() { return progress; @@ -69,23 +69,23 @@ public void setCourse(Course course) { this.course = course; } - public Set getCompetencies() { + public Set getCompetencies() { return competencies; } - public void setCompetencies(Set competencies) { + public void setCompetencies(Set competencies) { this.competencies = competencies; } - public void addCompetency(Competency competency) { + public void addCompetency(CourseCompetency competency) { this.competencies.add(competency); } - public void addCompetencies(Set competencies) { + public void addCompetencies(Set competencies) { this.competencies.addAll(competencies); } - public void removeCompetency(Competency competency) { + public void removeCompetency(CourseCompetency competency) { this.competencies.remove(competency); } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/competency/StandardizedCompetency.java b/src/main/java/de/tum/in/www1/artemis/domain/competency/StandardizedCompetency.java index 1da395866e01..8bb4a93e1f99 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/competency/StandardizedCompetency.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/competency/StandardizedCompetency.java @@ -61,7 +61,7 @@ public class StandardizedCompetency extends BaseCompetency { @OneToMany(mappedBy = "linkedStandardizedCompetency", fetch = FetchType.LAZY) @JsonIgnoreProperties("linkedStandardizedCompetency") - private Set linkedCompetencies = new HashSet<>(); + private Set linkedCompetencies = new HashSet<>(); public StandardizedCompetency(String title, String description, CompetencyTaxonomy taxonomy, String version) { super(title, description, taxonomy); @@ -112,11 +112,11 @@ public void setChildVersions(Set childVersions) { this.childVersions = childVersions; } - public Set getLinkedCompetencies() { + public Set getLinkedCompetencies() { return linkedCompetencies; } - public void setLinkedCompetencies(Set linkedCompetencies) { + public void setLinkedCompetencies(Set linkedCompetencies) { this.linkedCompetencies = linkedCompetencies; } } 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 8c307170b904..1d661b4eacfd 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 @@ -19,7 +19,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; 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.CourseCompetency; @Entity @DiscriminatorValue("E") @@ -66,12 +66,12 @@ public void setReleaseDate(ZonedDateTime releaseDate) { } @Override - public Set getCompetencies() { + public Set getCompetencies() { return exercise == null || !Hibernate.isPropertyInitialized(exercise, "competencies") ? new HashSet<>() : exercise.getCompetencies(); } @Override - public void setCompetencies(Set competencies) { + public void setCompetencies(Set competencies) { // Should be set in associated exercise } 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 0b47e1a41f29..5e161274c904 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 @@ -37,7 +37,7 @@ import de.tum.in.www1.artemis.domain.LearningObject; 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.competency.CourseCompetency; @Entity @Table(name = "lecture_unit") @@ -78,7 +78,7 @@ public abstract class LectureUnit extends DomainObject implements LearningObject @OrderBy("title") @JsonIgnoreProperties({ "lectureUnits", "course" }) @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) - protected Set competencies = new HashSet<>(); + protected Set competencies = new HashSet<>(); @OneToMany(mappedBy = "lectureUnit", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE, orphanRemoval = true) @JsonIgnore // important, so that the completion status of other users do not leak to anyone @@ -109,11 +109,11 @@ public void setReleaseDate(ZonedDateTime releaseDate) { } @Override - public Set getCompetencies() { + public Set getCompetencies() { return competencies; } - public void setCompetencies(Set competencies) { + public void setCompetencies(Set competencies) { this.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 4d044dc43467..eb3042183880 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 @@ -15,8 +15,8 @@ import org.springframework.transaction.annotation.Transactional; 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.CompetencyProgress; +import de.tum.in.www1.artemis.domain.competency.CourseCompetency; import de.tum.in.www1.artemis.repository.base.ArtemisJpaRepository; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; @@ -53,7 +53,7 @@ default CompetencyProgress findByCompetencyIdAndUserIdOrElseThrow(long competenc WHERE cp.competency IN :competencies AND cp.user.id = :userId """) - Set findByCompetenciesAndUser(@Param("competencies") Collection competencies, @Param("userId") long userId); + Set findByCompetenciesAndUser(@Param("competencies") Collection competencies, @Param("userId") long userId); @Query(""" SELECT cp @@ -90,13 +90,13 @@ SELECT COUNT(cp) @Query(""" SELECT cp - FROM Competency c + FROM CourseCompetency c LEFT JOIN CompetencyRelation cr ON cr.tailCompetency = c - LEFT JOIN Competency priorC ON priorC = cr.headCompetency + LEFT JOIN CourseCompetency priorC ON priorC = cr.headCompetency LEFT JOIN FETCH CompetencyProgress cp ON cp.competency = priorC WHERE cr.type <> de.tum.in.www1.artemis.domain.competency.RelationType.MATCHES AND cp.user = :user AND c = :competency """) - Set findAllPriorByCompetencyId(@Param("competency") Competency competency, @Param("user") User userId); + Set findAllPriorByCompetencyId(@Param("competency") CourseCompetency competency, @Param("user") User userId); } 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 cbbde4077ef4..a064690fce2a 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 @@ -16,11 +16,9 @@ import org.springframework.stereotype.Repository; 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.base.ArtemisJpaRepository; -import de.tum.in.www1.artemis.web.rest.dto.metrics.CompetencyExerciseMasteryCalculationDTO; /** * Spring Data JPA repository for the Competency entity. @@ -53,36 +51,6 @@ public interface CompetencyRepository extends ArtemisJpaRepository findWithLectureUnitsAndExercisesById(@Param("competencyId") long competencyId); - /** - * Fetches all information related to the calculation of the mastery for exercises in a competency. - * The complex grouping by is necessary for postgres - * - * @param competencyId the id of the competency for which to fetch the exercise information - * @param user the user for which to fetch the exercise information - * @return the exercise information for the calculation of the mastery in the competency - */ - @Query(""" - SELECT new de.tum.in.www1.artemis.web.rest.dto.metrics.CompetencyExerciseMasteryCalculationDTO( - ex.maxPoints, - ex.difficulty, - CASE WHEN TYPE(ex) = ProgrammingExercise THEN TRUE ELSE FALSE END, - COALESCE(sS.lastScore, tS.lastScore), - COALESCE(sS.lastPoints, tS.lastPoints), - COALESCE(sS.lastModifiedDate, tS.lastModifiedDate), - COUNT(s) - ) - FROM Competency c - LEFT JOIN c.exercises ex - LEFT JOIN ex.studentParticipations sp ON sp.student = :user OR :user MEMBER OF sp.team.students - LEFT JOIN sp.submissions s - LEFT JOIN StudentScore sS ON sS.exercise = ex AND sS.user = :user - LEFT JOIN TeamScore tS ON tS.exercise = ex AND :user MEMBER OF tS.team.students - WHERE c.id = :competencyId - AND ex IS NOT NULL - GROUP BY ex.maxPoints, ex.difficulty, TYPE(ex), sS.lastScore, tS.lastScore, sS.lastPoints, tS.lastPoints, sS.lastModifiedDate, tS.lastModifiedDate - """) - Set findAllExerciseInfoByCompetencyId(@Param("competencyId") long competencyId, @Param("user") User user); - @Query(""" SELECT c FROM Competency c @@ -169,15 +137,6 @@ Page findForImport(@Param("partialTitle") String partialTitle, @Para """) Set findAllByLearningPath(@Param("learningPath") LearningPath learningPath); - @Query(""" - SELECT c - FROM Competency c - LEFT JOIN FETCH c.lectureUnits lu - LEFT JOIN FETCH c.exercises ex - WHERE c.id = :competencyId - """) - Optional findByIdWithExercisesAndLectureUnits(@Param("competencyId") long competencyId); - default Competency findByIdWithExercisesElseThrow(long competencyId) { return getValueElseThrow(findByIdWithExercises(competencyId), competencyId); } @@ -194,10 +153,6 @@ default Competency findByIdWithLectureUnitsElseThrow(long competencyId) { return getValueElseThrow(findByIdWithLectureUnits(competencyId), competencyId); } - default Competency findByIdWithExercisesAndLectureUnitsElseThrow(long competencyId) { - return getValueElseThrow(findByIdWithExercisesAndLectureUnits(competencyId), competencyId); - } - long countByCourse(Course course); List findByCourseIdOrderById(long courseId); diff --git a/src/main/java/de/tum/in/www1/artemis/repository/CourseCompetencyRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/CourseCompetencyRepository.java index 5b35d63ab1e8..cfa2e1ac9ce3 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/CourseCompetencyRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/CourseCompetencyRepository.java @@ -1,19 +1,22 @@ package de.tum.in.www1.artemis.repository; 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.Query; import org.springframework.data.repository.query.Param; +import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.competency.CourseCompetency; +import de.tum.in.www1.artemis.repository.base.ArtemisJpaRepository; +import de.tum.in.www1.artemis.web.rest.dto.metrics.CompetencyExerciseMasteryCalculationDTO; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; /** * Spring Data JPA repository for the {@link CourseCompetency} entity. */ -public interface CourseCompetencyRepository extends JpaRepository { +public interface CourseCompetencyRepository extends ArtemisJpaRepository { @Query(""" SELECT c @@ -29,6 +32,60 @@ public interface CourseCompetencyRepository extends JpaRepository findAllTitlesByCourseId(@Param("courseId") long courseId); + @Query(""" + SELECT c + FROM CourseCompetency c + LEFT JOIN FETCH c.lectureUnits lu + WHERE c.id = :competencyId + """) + Optional findByIdWithLectureUnits(@Param("competencyId") long competencyId); + + @Query(""" + SELECT c + FROM CourseCompetency c + WHERE c.course.id = :courseId + """) + Set findAllForCourse(@Param("courseId") long courseId); + + /** + * Fetches all information related to the calculation of the mastery for exercises in a competency. + * The complex grouping by is necessary for postgres + * + * @param competencyId the id of the competency for which to fetch the exercise information + * @param user the user for which to fetch the exercise information + * @return the exercise information for the calculation of the mastery in the competency + */ + @Query(""" + SELECT new de.tum.in.www1.artemis.web.rest.dto.metrics.CompetencyExerciseMasteryCalculationDTO( + ex.maxPoints, + ex.difficulty, + CASE WHEN TYPE(ex) = ProgrammingExercise THEN TRUE ELSE FALSE END, + COALESCE(sS.lastScore, tS.lastScore), + COALESCE(sS.lastPoints, tS.lastPoints), + COALESCE(sS.lastModifiedDate, tS.lastModifiedDate), + COUNT(s) + ) + FROM CourseCompetency c + LEFT JOIN c.exercises ex + LEFT JOIN ex.studentParticipations sp ON sp.student = :user OR :user MEMBER OF sp.team.students + LEFT JOIN sp.submissions s + LEFT JOIN StudentScore sS ON sS.exercise = ex AND sS.user = :user + LEFT JOIN TeamScore tS ON tS.exercise = ex AND :user MEMBER OF tS.team.students + WHERE c.id = :competencyId + AND ex IS NOT NULL + GROUP BY ex.maxPoints, ex.difficulty, TYPE(ex), sS.lastScore, tS.lastScore, sS.lastPoints, tS.lastPoints, sS.lastModifiedDate, tS.lastModifiedDate + """) + Set findAllExerciseInfoByCompetencyId(@Param("competencyId") long competencyId, @Param("user") User user); + + @Query(""" + SELECT c + FROM CourseCompetency c + LEFT JOIN FETCH c.lectureUnits lu + LEFT JOIN FETCH c.exercises ex + WHERE c.id = :competencyId + """) + Optional findByIdWithExercisesAndLectureUnits(@Param("competencyId") long competencyId); + /** * Finds a list of competencies by id and verifies that the user is at least editor in the respective courses. * If any of the competencies are not accessible, throws a {@link EntityNotFoundException} @@ -58,4 +115,14 @@ default List findAllByIdElseThrow(List courseCompetencyI } return courseCompetencies; } + + default CourseCompetency findByIdWithLectureUnitsElseThrow(long competencyId) { + return getValueElseThrow(findByIdWithLectureUnits(competencyId), competencyId); + } + + default CourseCompetency findByIdWithExercisesAndLectureUnitsElseThrow(long competencyId) { + return getValueElseThrow(findByIdWithExercisesAndLectureUnits(competencyId), competencyId); + } + + List findByCourseIdOrderById(long courseId); } 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 9c3b43ff2320..2edfac2b94c1 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 @@ -127,7 +127,7 @@ SELECT COUNT(c) > 0 Course findWithEagerExercisesById(long courseId); @EntityGraph(type = LOAD, attributePaths = { "competencies", "prerequisites" }) - Optional findWithEagerCompetenciesById(long courseId); + Optional findWithEagerCompetenciesAndPrerequisitesById(long courseId); @EntityGraph(type = LOAD, attributePaths = { "learningPaths" }) Optional findWithEagerLearningPathsById(long courseId); @@ -182,10 +182,11 @@ SELECT COUNT(c) > 0 FROM Course course LEFT JOIN FETCH course.organizations LEFT JOIN FETCH course.competencies + LEFT JOIN FETCH course.prerequisites LEFT JOIN FETCH course.learningPaths WHERE course.id = :courseId """) - Optional findWithEagerOrganizationsAndCompetenciesAndLearningPaths(@Param("courseId") long courseId); + Optional findWithEagerOrganizationsAndCompetenciesAndPrerequisitesAndLearningPaths(@Param("courseId") long courseId); @EntityGraph(type = LOAD, attributePaths = { "onlineCourseConfiguration", "tutorialGroupsConfiguration" }) Course findWithEagerOnlineCourseConfigurationAndTutorialGroupConfigurationById(long courseId); @@ -378,8 +379,8 @@ default Course findWithEagerOrganizationsElseThrow(long courseId) throws EntityN } @NotNull - default Course findWithEagerOrganizationsAndCompetenciesAndLearningPathsElseThrow(long courseId) throws EntityNotFoundException { - return findWithEagerOrganizationsAndCompetenciesAndLearningPaths(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId)); + default Course findWithEagerOrganizationsAndCompetenciesAndPrerequisitesAndLearningPathsElseThrow(long courseId) throws EntityNotFoundException { + return getValueElseThrow(findWithEagerOrganizationsAndCompetenciesAndPrerequisitesAndLearningPaths(courseId), courseId); } /** @@ -475,8 +476,8 @@ default Course findByIdWithExercisesAndLecturesAndLectureUnitsAndCompetenciesEls } @NotNull - default Course findWithEagerCompetenciesByIdElseThrow(long courseId) { - return findWithEagerCompetenciesById(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId)); + default Course findWithEagerCompetenciesAndPrerequisitesByIdElseThrow(long courseId) { + return getValueElseThrow(findWithEagerCompetenciesAndPrerequisitesById(courseId), courseId); } @NotNull 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 bf6a44d66142..ae376cd6b124 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 @@ -586,7 +586,7 @@ 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()); + course = courseRepository.findWithEagerCompetenciesAndPrerequisitesByIdElseThrow(course.getId()); } String courseGroupName = course.defineCourseGroupName(courseGroup); Role courseGroupRole = Role.fromString(courseGroup); diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningObjectService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningObjectService.java index 8d2bcf6013b0..63c321cb467b 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningObjectService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningObjectService.java @@ -16,7 +16,7 @@ import de.tum.in.www1.artemis.domain.Exercise; 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.CourseCompetency; import de.tum.in.www1.artemis.domain.lecture.LectureUnit; import de.tum.in.www1.artemis.domain.lecture.LectureUnitCompletion; import de.tum.in.www1.artemis.repository.ExerciseRepository; @@ -75,8 +75,8 @@ public boolean isCompletedByUser(@NotNull LearningObject learningObject, @NotNul * @param competencies the competencies for which to get the completed learning objects * @return the completed learning objects for the given user and competencies */ - public Stream getCompletedLearningObjectsForUserAndCompetencies(User user, Set competencies) { - return Stream.concat(competencies.stream().map(Competency::getLectureUnits), competencies.stream().map(Competency::getExercises)).flatMap(Set::stream) + public Stream getCompletedLearningObjectsForUserAndCompetencies(User user, Set competencies) { + return Stream.concat(competencies.stream().map(CourseCompetency::getLectureUnits), competencies.stream().map(CourseCompetency::getExercises)).flatMap(Set::stream) .filter(learningObject -> learningObject.getCompletionDate(user).isPresent()) .sorted(Comparator.comparing(learningObject -> learningObject.getCompletionDate(user).orElseThrow())).map(LearningObject.class::cast); } 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 23e6b3b849de..725533ccdebb 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 @@ -25,12 +25,13 @@ 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.competency.CourseCompetency; import de.tum.in.www1.artemis.domain.lecture.AttachmentUnit; 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; import de.tum.in.www1.artemis.domain.lecture.Slide; -import de.tum.in.www1.artemis.repository.CompetencyRepository; +import de.tum.in.www1.artemis.repository.CourseCompetencyRepository; import de.tum.in.www1.artemis.repository.ExerciseRepository; import de.tum.in.www1.artemis.repository.LectureRepository; import de.tum.in.www1.artemis.repository.LectureUnitCompletionRepository; @@ -46,8 +47,6 @@ public class LectureUnitService { private final LectureRepository lectureRepository; - private final CompetencyRepository competencyRepository; - private final LectureUnitCompletionRepository lectureUnitCompletionRepository; private final FileService fileService; @@ -58,17 +57,19 @@ public class LectureUnitService { private final Optional pyrisWebhookService; - public LectureUnitService(LectureUnitRepository lectureUnitRepository, LectureRepository lectureRepository, CompetencyRepository competencyRepository, - LectureUnitCompletionRepository lectureUnitCompletionRepository, FileService fileService, SlideRepository slideRepository, ExerciseRepository exerciseRepository, - Optional pyrisWebhookService) { + private final CourseCompetencyRepository courseCompetencyRepository; + + public LectureUnitService(LectureUnitRepository lectureUnitRepository, LectureRepository lectureRepository, LectureUnitCompletionRepository lectureUnitCompletionRepository, + FileService fileService, SlideRepository slideRepository, ExerciseRepository exerciseRepository, Optional pyrisWebhookService, + CourseCompetencyRepository courseCompetencyRepository) { this.lectureUnitRepository = lectureUnitRepository; this.lectureRepository = lectureRepository; - this.competencyRepository = competencyRepository; this.lectureUnitCompletionRepository = lectureUnitCompletionRepository; this.fileService = fileService; this.slideRepository = slideRepository; this.exerciseRepository = exerciseRepository; this.pyrisWebhookService = pyrisWebhookService; + this.courseCompetencyRepository = courseCompetencyRepository; } /** @@ -152,9 +153,9 @@ public void removeLectureUnit(@NotNull LectureUnit lectureUnit) { if (!(lectureUnitToDelete instanceof ExerciseUnit)) { // update associated competencies - Set competencies = lectureUnitToDelete.getCompetencies(); - competencyRepository.saveAll(competencies.stream().map(competency -> { - competency = competencyRepository.findByIdWithLectureUnitsElseThrow(competency.getId()); + Set competencies = lectureUnitToDelete.getCompetencies(); + courseCompetencyRepository.saveAll(competencies.stream().map(competency -> { + competency = courseCompetencyRepository.findByIdWithLectureUnitsElseThrow(competency.getId()); competency.getLectureUnits().remove(lectureUnitToDelete); return competency; }).toList()); diff --git a/src/main/java/de/tum/in/www1/artemis/service/competency/CompetencyProgressService.java b/src/main/java/de/tum/in/www1/artemis/service/competency/CompetencyProgressService.java index 0624d54b2bba..f7194d745fc8 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/competency/CompetencyProgressService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/competency/CompetencyProgressService.java @@ -22,15 +22,15 @@ import de.tum.in.www1.artemis.domain.Exercise; 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.CompetencyProgress; +import de.tum.in.www1.artemis.domain.competency.CourseCompetency; import de.tum.in.www1.artemis.domain.enumeration.CompetencyProgressConfidenceReason; import de.tum.in.www1.artemis.domain.enumeration.DifficultyLevel; 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.participation.Participant; import de.tum.in.www1.artemis.repository.CompetencyProgressRepository; -import de.tum.in.www1.artemis.repository.CompetencyRepository; +import de.tum.in.www1.artemis.repository.CourseCompetencyRepository; import de.tum.in.www1.artemis.repository.ExerciseRepository; import de.tum.in.www1.artemis.repository.LectureUnitCompletionRepository; import de.tum.in.www1.artemis.repository.LectureUnitRepository; @@ -51,8 +51,6 @@ public class CompetencyProgressService { private static final Logger log = LoggerFactory.getLogger(CompetencyProgressService.class); - private final CompetencyRepository competencyRepository; - private final CompetencyProgressRepository competencyProgressRepository; private final ExerciseRepository exerciseRepository; @@ -67,6 +65,8 @@ public class CompetencyProgressService { private final LectureUnitCompletionRepository lectureUnitCompletionRepository; + private final CourseCompetencyRepository courseCompetencyRepository; + private static final int MIN_EXERCISES_RECENCY_CONFIDENCE = 3; private static final int MAX_SUBMISSIONS_FOR_QUICK_SOLVE_HEURISTIC = 3; @@ -77,10 +77,9 @@ public class CompetencyProgressService { private static final double CONFIDENCE_REASON_DEADZONE = 0.05; - public CompetencyProgressService(CompetencyRepository competencyRepository, CompetencyProgressRepository competencyProgressRepository, ExerciseRepository exerciseRepository, - LectureUnitRepository lectureUnitRepository, UserRepository userRepository, LearningPathService learningPathService, ParticipantScoreService participantScoreService, - LectureUnitCompletionRepository lectureUnitCompletionRepository) { - this.competencyRepository = competencyRepository; + public CompetencyProgressService(CompetencyProgressRepository competencyProgressRepository, ExerciseRepository exerciseRepository, LectureUnitRepository lectureUnitRepository, + UserRepository userRepository, LearningPathService learningPathService, ParticipantScoreService participantScoreService, + LectureUnitCompletionRepository lectureUnitCompletionRepository, CourseCompetencyRepository courseCompetencyRepository) { this.competencyProgressRepository = competencyProgressRepository; this.exerciseRepository = exerciseRepository; this.lectureUnitRepository = lectureUnitRepository; @@ -88,6 +87,7 @@ public CompetencyProgressService(CompetencyRepository competencyRepository, Comp this.learningPathService = learningPathService; this.participantScoreService = participantScoreService; this.lectureUnitCompletionRepository = lectureUnitCompletionRepository; + this.courseCompetencyRepository = courseCompetencyRepository; } /** @@ -110,12 +110,11 @@ public void updateProgressByLearningObjectAsync(LearningObject learningObject, @ @Async public void updateProgressByLearningObjectAsync(LearningObject learningObject) { SecurityUtils.setAuthorizationObject(); // required for async - Course course; - switch (learningObject) { - case Exercise exercise -> course = exercise.getCourseViaExerciseGroupOrCourseMember(); - case LectureUnit lectureUnit -> course = lectureUnit.getLecture().getCourse(); + Course course = switch (learningObject) { + case Exercise exercise -> exercise.getCourseViaExerciseGroupOrCourseMember(); + case LectureUnit lectureUnit -> lectureUnit.getLecture().getCourse(); default -> throw new IllegalArgumentException("Learning object must be either LectureUnit or Exercise"); - } + }; updateProgressByLearningObject(learningObject, userRepository.getStudents(course)); } @@ -125,7 +124,7 @@ public void updateProgressByLearningObjectAsync(LearningObject learningObject) { * @param competency The competency for which to update all existing student progress */ @Async - public void updateProgressByCompetencyAsync(Competency competency) { + public void updateProgressByCompetencyAsync(CourseCompetency competency) { SecurityUtils.setAuthorizationObject(); // required for async competencyProgressRepository.findAllByCompetencyId(competency.getId()).stream().map(CompetencyProgress::getUser) .forEach(user -> updateCompetencyProgress(competency.getId(), user)); @@ -140,12 +139,11 @@ public void updateProgressByCompetencyAsync(Competency competency) { public void updateProgressByLearningObject(LearningObject learningObject, @NotNull Set users) { log.debug("Updating competency progress for {} users.", users.size()); try { - Set competencies; - switch (learningObject) { - case Exercise exercise -> competencies = exerciseRepository.findWithCompetenciesById(exercise.getId()).map(Exercise::getCompetencies).orElse(null); - case LectureUnit lectureUnit -> competencies = lectureUnitRepository.findWithCompetenciesById(lectureUnit.getId()).map(LectureUnit::getCompetencies).orElse(null); + Set competencies = switch (learningObject) { + case Exercise exercise -> exerciseRepository.findWithCompetenciesById(exercise.getId()).map(Exercise::getCompetencies).orElse(null); + case LectureUnit lectureUnit -> lectureUnitRepository.findWithCompetenciesById(lectureUnit.getId()).map(LectureUnit::getCompetencies).orElse(null); default -> throw new IllegalArgumentException("Learning object must be either LectureUnit or Exercise"); - } + }; if (competencies == null) { // Competencies couldn't be loaded, the exercise/lecture unit might have already been deleted @@ -168,16 +166,16 @@ public void updateProgressByLearningObject(LearningObject learningObject, @NotNu * @return The updated competency progress, which is also persisted to the database */ public CompetencyProgress updateCompetencyProgress(Long competencyId, User user) { - Optional optionalCompetency = competencyRepository.findByIdWithLectureUnits(competencyId); + Optional optionalCompetency = courseCompetencyRepository.findByIdWithLectureUnits(competencyId); if (user == null || optionalCompetency.isEmpty()) { log.debug("User or competency no longer exist, skipping."); return null; } - Competency competency = optionalCompetency.get(); + CourseCompetency competency = optionalCompetency.get(); Set lectureUnits = competency.getLectureUnits().stream().filter(lectureUnit -> !(lectureUnit instanceof ExerciseUnit)).collect(Collectors.toSet()); - Set exerciseInfos = competencyRepository.findAllExerciseInfoByCompetencyId(competencyId, user); + Set exerciseInfos = courseCompetencyRepository.findAllExerciseInfoByCompetencyId(competencyId, user); int numberOfCompletedLectureUnits = lectureUnitCompletionRepository .countByLectureUnitIdsAndUserId(competency.getLectureUnits().stream().map(LectureUnit::getId).collect(Collectors.toSet()), user.getId()); @@ -448,7 +446,7 @@ public static boolean isMastered(@NotNull CompetencyProgress competencyProgress) * @param competency the competency to check * @return true if the competency can be mastered without completing any exercises, false otherwise */ - public static boolean canBeMasteredWithoutExercises(@NotNull Competency competency) { + public static boolean canBeMasteredWithoutExercises(@NotNull CourseCompetency competency) { double numberOfLectureUnits = competency.getLectureUnits().size(); double numberOfLearningObjects = numberOfLectureUnits + competency.getExercises().size(); if (numberOfLearningObjects == 0) { @@ -476,7 +474,7 @@ public void deleteProgressForCompetency(long competencyId) { * @param course The course for which to get the progress * @return The progress for the course */ - public CourseCompetencyProgressDTO getCompetencyCourseProgress(@NotNull Competency competency, @NotNull Course course) { + public CourseCompetencyProgressDTO getCompetencyCourseProgress(@NotNull CourseCompetency competency, @NotNull Course course) { var numberOfStudents = competencyProgressRepository.countByCompetency(competency.getId()); var numberOfMasteredStudents = competencyProgressRepository.countByCompetencyAndMastered(competency.getId(), competency.getMasteryThreshold()); var averageStudentScore = RoundingUtil.roundScoreSpecifiedByCourseSettings(participantScoreService.getAverageOfAverageScores(competency.getExercises()), course); diff --git a/src/main/java/de/tum/in/www1/artemis/service/competency/CompetencyRelationService.java b/src/main/java/de/tum/in/www1/artemis/service/competency/CompetencyRelationService.java index 1e656e11806b..0d17fad767ab 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/competency/CompetencyRelationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/competency/CompetencyRelationService.java @@ -8,11 +8,12 @@ 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.CompetencyRelation; +import de.tum.in.www1.artemis.domain.competency.CourseCompetency; import de.tum.in.www1.artemis.domain.competency.RelationType; import de.tum.in.www1.artemis.repository.CompetencyRelationRepository; import de.tum.in.www1.artemis.repository.CompetencyRepository; +import de.tum.in.www1.artemis.repository.CourseCompetencyRepository; /** * Service for managing CompetencyRelations. @@ -28,10 +29,14 @@ public class CompetencyRelationService { private final CompetencyRepository competencyRepository; - public CompetencyRelationService(CompetencyRelationRepository competencyRelationRepository, CompetencyService competencyService, CompetencyRepository competencyRepository) { + private final CourseCompetencyRepository courseCompetencyRepository; + + public CompetencyRelationService(CompetencyRelationRepository competencyRelationRepository, CompetencyService competencyService, CompetencyRepository competencyRepository, + CourseCompetencyRepository courseCompetencyRepository) { this.competencyRelationRepository = competencyRelationRepository; this.competencyService = competencyService; this.competencyRepository = competencyRepository; + this.courseCompetencyRepository = courseCompetencyRepository; } /** @@ -44,7 +49,7 @@ public CompetencyRelationService(CompetencyRelationRepository competencyRelation * @param relationType the type of the relation * @return the created CompetencyRelation */ - public CompetencyRelation getCompetencyRelation(Competency tailCompetency, Competency headCompetency, RelationType relationType) { + public CompetencyRelation getCompetencyRelation(CourseCompetency tailCompetency, CourseCompetency headCompetency, RelationType relationType) { CompetencyRelation competencyRelation = new CompetencyRelation(); competencyRelation.setTailCompetency(tailCompetency); competencyRelation.setHeadCompetency(headCompetency); @@ -61,12 +66,12 @@ public CompetencyRelation getCompetencyRelation(Competency tailCompetency, Compe * @param course the course the relation belongs to * @return the persisted CompetencyRelation */ - public CompetencyRelation createCompetencyRelation(Competency tailCompetency, Competency headCompetency, RelationType relationType, Course course) { + public CompetencyRelation createCompetencyRelation(CourseCompetency tailCompetency, CourseCompetency headCompetency, RelationType relationType, Course course) { if (relationType == null) { throw new BadRequestException("Competency relation must have a relation type"); } var relation = getCompetencyRelation(tailCompetency, headCompetency, relationType); - var competencies = competencyRepository.findAllForCourse(course.getId()); + var competencies = courseCompetencyRepository.findAllForCourse(course.getId()); var competencyRelations = competencyRelationRepository.findAllWithHeadAndTailByCourseId(course.getId()); competencyRelations.add(relation); diff --git a/src/main/java/de/tum/in/www1/artemis/service/competency/CompetencyService.java b/src/main/java/de/tum/in/www1/artemis/service/competency/CompetencyService.java index ceeffe5f6ce3..732eab4d22cd 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/competency/CompetencyService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/competency/CompetencyService.java @@ -21,11 +21,13 @@ 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.CourseCompetency; import de.tum.in.www1.artemis.domain.competency.RelationType; import de.tum.in.www1.artemis.domain.competency.StandardizedCompetency; import de.tum.in.www1.artemis.repository.CompetencyProgressRepository; import de.tum.in.www1.artemis.repository.CompetencyRelationRepository; import de.tum.in.www1.artemis.repository.CompetencyRepository; +import de.tum.in.www1.artemis.repository.CourseCompetencyRepository; import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.LectureUnitCompletionRepository; import de.tum.in.www1.artemis.repository.competency.StandardizedCompetencyRepository; @@ -74,10 +76,12 @@ public class CompetencyService { private final CourseRepository courseRepository; + private final CourseCompetencyRepository courseCompetencyRepository; + public CompetencyService(CompetencyRepository competencyRepository, AuthorizationCheckService authCheckService, CompetencyRelationRepository competencyRelationRepository, LearningPathService learningPathService, CompetencyProgressService competencyProgressService, LectureUnitService lectureUnitService, ExerciseService exerciseService, CompetencyProgressRepository competencyProgressRepository, LectureUnitCompletionRepository lectureUnitCompletionRepository, - StandardizedCompetencyRepository standardizedCompetencyRepository, CourseRepository courseRepository) { + StandardizedCompetencyRepository standardizedCompetencyRepository, CourseRepository courseRepository, CourseCompetencyRepository courseCompetencyRepository) { this.competencyRepository = competencyRepository; this.authCheckService = authCheckService; this.competencyRelationRepository = competencyRelationRepository; @@ -89,6 +93,7 @@ public CompetencyService(CompetencyRepository competencyRepository, Authorizatio this.lectureUnitCompletionRepository = lectureUnitCompletionRepository; this.standardizedCompetencyRepository = standardizedCompetencyRepository; this.courseRepository = courseRepository; + this.courseCompetencyRepository = courseCompetencyRepository; } /** @@ -394,7 +399,7 @@ public List competenciesToCompetencyWithTailRelat * @param relations The set of relations that get checked for cycles * @return A boolean that states whether the provided competencies and relations contain a cycle */ - public boolean doesCreateCircularRelation(Set competencies, Set relations) { + public boolean doesCreateCircularRelation(Set competencies, Set relations) { // Inner class Vertex is only used in this method for cycle detection class Vertex { @@ -479,7 +484,7 @@ public boolean vertexIsPartOfCycle(Vertex sourceVertex) { } var graph = new Graph(); - for (Competency competency : competencies) { + for (CourseCompetency competency : competencies) { graph.addVertex(new Vertex(competency.getTitle())); } for (CompetencyRelation relation : relations) { diff --git a/src/main/java/de/tum/in/www1/artemis/service/competency/CourseCompetencyService.java b/src/main/java/de/tum/in/www1/artemis/service/competency/CourseCompetencyService.java new file mode 100644 index 000000000000..e2a0ba12b833 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/competency/CourseCompetencyService.java @@ -0,0 +1,58 @@ +package de.tum.in.www1.artemis.service.competency; + +import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import de.tum.in.www1.artemis.domain.competency.CourseCompetency; +import de.tum.in.www1.artemis.repository.CompetencyProgressRepository; +import de.tum.in.www1.artemis.repository.CourseCompetencyRepository; + +/** + * Service for managing competencies. + */ +@Profile(PROFILE_CORE) +@Service +public class CourseCompetencyService { + + private final CompetencyProgressRepository competencyProgressRepository; + + private final CourseCompetencyRepository courseCompetencyRepository; + + public CourseCompetencyService(CompetencyProgressRepository competencyProgressRepository, CourseCompetencyRepository courseCompetencyRepository) { + this.competencyProgressRepository = competencyProgressRepository; + this.courseCompetencyRepository = courseCompetencyRepository; + } + + /** + * Finds competencies within a course and fetch progress for the provided user. + *

+ * As Spring Boot 3 doesn't support conditional JOIN FETCH statements, we have to retrieve the data manually. + * + * @param courseId The id of the course for which to fetch the competencies + * @param userId The id of the user for which to fetch the progress + * @return The found competency + */ + public List findCourseCompetenciesWithProgressForUserByCourseId(Long courseId, Long userId) { + List competencies = courseCompetencyRepository.findByCourseIdOrderById(courseId); + var progress = competencyProgressRepository.findByCompetenciesAndUser(competencies, userId).stream() + .collect(Collectors.toMap(completion -> completion.getCompetency().getId(), completion -> completion)); + + competencies.forEach(competency -> { + if (progress.containsKey(competency.getId())) { + competency.setUserProgress(Set.of(progress.get(competency.getId()))); + } + else { + competency.setUserProgress(Collections.emptySet()); + } + }); + + return competencies; + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathNavigationService.java b/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathNavigationService.java index e75bd63a7f7b..c6b56e8a4673 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathNavigationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathNavigationService.java @@ -10,7 +10,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.Competency; +import de.tum.in.www1.artemis.domain.competency.CourseCompetency; import de.tum.in.www1.artemis.domain.competency.LearningPath; import de.tum.in.www1.artemis.service.LearningObjectService; import de.tum.in.www1.artemis.service.learningpath.LearningPathRecommendationService.RecommendationState; @@ -92,7 +92,7 @@ private LearningPathNavigationDTO getNavigationRelativeToLearningObject(Recommen return new LearningPathNavigationDTO(predecessorLearningObjectDTO, currentLearningObjectDTO, successorLearningObjectDTO, learningPath.getProgress()); } - private LearningObject getPredecessorOfLearningObject(RecommendationState recommendationState, Competency currentCompetency, + private LearningObject getPredecessorOfLearningObject(RecommendationState recommendationState, CourseCompetency currentCompetency, List learningObjectsInCurrentCompetency, int indexOfCurrentLearningObject, User user) { LearningObject predecessorLearningObject = null; if (indexOfCurrentLearningObject == 0) { @@ -113,7 +113,7 @@ private LearningObject getPredecessorOfLearningObject(RecommendationState recomm return predecessorLearningObject; } - private LearningObject getSuccessorOfLearningObject(RecommendationState recommendationState, Competency currentCompetency, + private LearningObject getSuccessorOfLearningObject(RecommendationState recommendationState, CourseCompetency currentCompetency, List learningObjectsInCurrentCompetency, int indexOfCurrentLearningObject, User user) { LearningObject successorLearningObject = null; if (indexOfCurrentLearningObject == learningObjectsInCurrentCompetency.size() - 1) { diff --git a/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathNgxService.java b/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathNgxService.java index 97d5a270c9a8..14f53b277f26 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathNgxService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathNgxService.java @@ -20,8 +20,8 @@ import de.tum.in.www1.artemis.domain.Exercise; 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; +import de.tum.in.www1.artemis.domain.competency.CourseCompetency; import de.tum.in.www1.artemis.domain.competency.LearningPath; import de.tum.in.www1.artemis.domain.competency.RelationType; import de.tum.in.www1.artemis.domain.lecture.LectureUnit; @@ -76,7 +76,7 @@ public NgxLearningPathDTO generateNgxGraphRepresentation(@NotNull LearningPath l * @param nodes set of nodes to store the new nodes * @param edges set of edges to store the new edges */ - private void generateNgxGraphRepresentationForCompetency(LearningPath learningPath, Competency competency, Set nodes, + private void generateNgxGraphRepresentationForCompetency(LearningPath learningPath, CourseCompetency competency, Set nodes, Set edges) { Set currentCluster = new HashSet<>(); // generates start and end node @@ -253,7 +253,7 @@ public NgxLearningPathDTO generateNgxPathRepresentation(@NotNull LearningPath le * @param nodes set of nodes to store the new nodes * @param edges set of edges to store the new edges */ - private void generateNgxPathRepresentationForCompetency(User user, Competency competency, Set nodes, Set edges, + private void generateNgxPathRepresentationForCompetency(User user, CourseCompetency competency, Set nodes, Set edges, LearningPathRecommendationService.RecommendationState state) { Set currentCluster = new HashSet<>(); // generates start and end node @@ -289,7 +289,7 @@ private void generateNgxPathRepresentationForCompetency(User user, Competency co nodes.addAll(currentCluster); } - private static void addNodeForLearningObject(Competency competency, LearningObject learningObject, Set nodes) { + private static void addNodeForLearningObject(CourseCompetency competency, LearningObject learningObject, Set nodes) { if (learningObject instanceof LectureUnit lectureUnit) { nodes.add(NgxLearningPathDTO.Node.of(getLectureUnitNodeId(competency.getId(), lectureUnit.getId()), NgxLearningPathDTO.NodeType.LECTURE_UNIT, lectureUnit.getId(), lectureUnit.getLecture().getId(), false, lectureUnit.getName())); @@ -300,17 +300,17 @@ else if (learningObject instanceof Exercise exercise) { } } - private static void addEdgeBetweenLearningObjects(Competency competency, LearningObject source, LearningObject target, Set edges) { + private static void addEdgeBetweenLearningObjects(CourseCompetency competency, LearningObject source, LearningObject target, Set edges) { final var sourceId = getLearningObjectNodeId(competency.getId(), source); final var targetId = getLearningObjectNodeId(competency.getId(), target); edges.add(new NgxLearningPathDTO.Edge(getEdgeFromToId(sourceId, targetId), sourceId, targetId)); } - private static void addEdgeFromCompetencyStartToLearningObject(Competency competency, LearningObject learningObject, Set edges) { + private static void addEdgeFromCompetencyStartToLearningObject(CourseCompetency competency, LearningObject learningObject, Set edges) { addEdgeFromSourceToTarget(getCompetencyStartNodeId(competency.getId()), getLearningObjectNodeId(competency.getId(), learningObject), edges); } - private static void addEdgeFromLearningObjectToCompetencyEnd(Competency competency, LearningObject learningObject, Set edges) { + private static void addEdgeFromLearningObjectToCompetencyEnd(CourseCompetency competency, LearningObject learningObject, Set edges) { addEdgeFromSourceToTarget(getLearningObjectNodeId(competency.getId(), learningObject), getCompetencyEndNodeId(competency.getId()), edges); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathRecommendationService.java b/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathRecommendationService.java index 74fb092608d9..0effd8f01370 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathRecommendationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathRecommendationService.java @@ -26,15 +26,15 @@ import de.tum.in.www1.artemis.domain.LearningObject; 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.competency.CompetencyProgress; +import de.tum.in.www1.artemis.domain.competency.CourseCompetency; import de.tum.in.www1.artemis.domain.competency.LearningPath; import de.tum.in.www1.artemis.domain.competency.RelationType; import de.tum.in.www1.artemis.domain.enumeration.DifficultyLevel; import de.tum.in.www1.artemis.domain.lecture.LectureUnit; import de.tum.in.www1.artemis.repository.CompetencyProgressRepository; import de.tum.in.www1.artemis.repository.CompetencyRelationRepository; -import de.tum.in.www1.artemis.repository.CompetencyRepository; +import de.tum.in.www1.artemis.repository.CourseCompetencyRepository; import de.tum.in.www1.artemis.service.LearningObjectService; import de.tum.in.www1.artemis.service.ParticipantScoreService; import de.tum.in.www1.artemis.service.competency.CompetencyProgressService; @@ -54,6 +54,8 @@ public class LearningPathRecommendationService { private final CompetencyProgressRepository competencyProgressRepository; + private final CourseCompetencyRepository courseCompetencyRepository; + /** * Base utility that is used to calculate a competencies' utility with respect to the earliest due date of the competency. */ @@ -91,15 +93,13 @@ public class LearningPathRecommendationService { private static final double[][] EXERCISE_DIFFICULTY_DISTRIBUTION_LUT = new double[][] { { 0.87, 0.12, 0.01 }, { 0.80, 0.18, 0.02 }, { 0.72, 0.25, 0.03 }, { 0.61, 0.33, 0.06 }, { 0.50, 0.40, 0.10 }, { 0.39, 0.45, 0.16 }, { 0.28, 0.48, 0.24 }, { 0.20, 0.47, 0.33 }, { 0.13, 0.43, 0.44 }, { 0.08, 0.37, 0.55 }, { 0.04, 0.29, 0.67 }, }; - private final CompetencyRepository competencyRepository; - protected LearningPathRecommendationService(CompetencyRelationRepository competencyRelationRepository, LearningObjectService learningObjectService, - ParticipantScoreService participantScoreService, CompetencyProgressRepository competencyProgressRepository, CompetencyRepository competencyRepository) { + ParticipantScoreService participantScoreService, CompetencyProgressRepository competencyProgressRepository, CourseCompetencyRepository courseCompetencyRepository) { this.competencyRelationRepository = competencyRelationRepository; this.learningObjectService = learningObjectService; this.participantScoreService = participantScoreService; this.competencyProgressRepository = competencyProgressRepository; - this.competencyRepository = competencyRepository; + this.courseCompetencyRepository = courseCompetencyRepository; } /** @@ -190,7 +190,7 @@ public Stream getUncompletedLearningObjects(LearningPath learnin * @param recommendationState the current state of the learning path recommendation * @return the competency of the given learning object */ - public Competency getCompetencyOfLearningObjectOnLearningPath(User user, LearningObject learningObject, RecommendationState recommendationState) { + public CourseCompetency getCompetencyOfLearningObjectOnLearningPath(User user, LearningObject learningObject, RecommendationState recommendationState) { return recommendationState.recommendedOrderOfCompetencies.stream().map(recommendationState.competencyIdMap::get) .filter(competency -> getOrderOfLearningObjectsForCompetency(competency, user).contains(learningObject)).findFirst().orElse(null); } @@ -203,7 +203,7 @@ public Competency getCompetencyOfLearningObjectOnLearningPath(User user, Learnin * @see RecommendationState */ private RecommendationState generateInitialRecommendationState(LearningPath learningPath) { - Map competencyIdMap = learningPath.getCompetencies().stream().collect(Collectors.toMap(Competency::getId, Function.identity())); + Map competencyIdMap = learningPath.getCompetencies().stream().collect(Collectors.toMap(CourseCompetency::getId, Function.identity())); Map> matchingClusters = getMatchingCompetencyClusters(learningPath.getCompetencies()); Map> priorsCompetencies = getPriorCompetencyMapping(learningPath.getCompetencies(), matchingClusters); Map extendsCompetencies = getExtendsCompetencyMapping(learningPath.getCompetencies(), matchingClusters, priorsCompetencies); @@ -236,7 +236,7 @@ else if (CompetencyProgressService.isMastered(progress.get())) { * @param competencies the competencies for which the mapping should be generated * @return map representing the matching clusters */ - private Map> getMatchingCompetencyClusters(Set competencies) { + private Map> getMatchingCompetencyClusters(Set competencies) { final Map> matchingClusters = new HashMap<>(); for (var competency : competencies) { if (!matchingClusters.containsKey(competency.getId())) { @@ -255,7 +255,7 @@ private Map> getMatchingCompetencyClusters(Set compe * @param matchingClusters the map representing the corresponding matching clusters * @return map to retrieve prior competencies */ - private Map> getPriorCompetencyMapping(Set competencies, Map> matchingClusters) { + private Map> getPriorCompetencyMapping(Set competencies, Map> matchingClusters) { Map> priorsMap = new HashMap<>(); for (var competency : competencies) { if (!priorsMap.containsKey(competency.getId())) { @@ -275,7 +275,7 @@ private Map> getPriorCompetencyMapping(Set competenc * @param priorCompetencies the map to retrieve corresponding prior competencies * @return map to retrieve the number of competencies a competency extends */ - private Map getExtendsCompetencyMapping(Set competencies, Map> matchingClusters, Map> priorCompetencies) { + private Map getExtendsCompetencyMapping(Set competencies, Map> matchingClusters, Map> priorCompetencies) { return getRelationsOfTypeCompetencyMapping(competencies, matchingClusters, priorCompetencies, RelationType.EXTENDS); } @@ -287,7 +287,7 @@ private Map getExtendsCompetencyMapping(Set competencies * @param priorCompetencies the map to retrieve corresponding prior competencies * @return map to retrieve the number of competencies a competency assumes */ - private Map getAssumesCompetencyMapping(Set competencies, Map> matchingClusters, Map> priorCompetencies) { + private Map getAssumesCompetencyMapping(Set competencies, Map> matchingClusters, Map> priorCompetencies) { return getRelationsOfTypeCompetencyMapping(competencies, matchingClusters, priorCompetencies, RelationType.ASSUMES); } @@ -300,7 +300,7 @@ private Map getAssumesCompetencyMapping(Set competencies * @param type the relation type that should be counted * @return map to retrieve the number of competencies a competency extends */ - private Map getRelationsOfTypeCompetencyMapping(Set competencies, Map> matchingClusters, Map> priorCompetencies, + private Map getRelationsOfTypeCompetencyMapping(Set competencies, Map> matchingClusters, Map> priorCompetencies, RelationType type) { Map map = new HashMap<>(); for (var competency : competencies) { @@ -321,7 +321,7 @@ private Map getRelationsOfTypeCompetencyMapping(Set comp * @param state the current state of the recommendation system * @return set of pending competencies */ - private Set getPendingCompetencies(Set competencies, RecommendationState state) { + private Set getPendingCompetencies(Set competencies, RecommendationState state) { return competencies.stream().filter(competency -> !state.masteredCompetencies.contains(competency.getId()) || state.matchingClusters.get(competency.getId()).stream().noneMatch(state.masteredCompetencies::contains)).collect(Collectors.toSet()); } @@ -332,7 +332,7 @@ private Set getPendingCompetencies(Set competencies, Rec * @param pendingCompetencies the set of pending competencies * @param state the current state of the recommendation system */ - private void simulateProgression(Set pendingCompetencies, RecommendationState state) { + private void simulateProgression(Set pendingCompetencies, RecommendationState state) { while (!pendingCompetencies.isEmpty()) { Map utilities = computeUtilities(pendingCompetencies, state); var maxEntry = utilities.entrySet().stream().max(Comparator.comparingDouble(Map.Entry::getValue)); @@ -356,7 +356,7 @@ private void simulateProgression(Set pendingCompetencies, Recommenda * @param state the current state of the recommendation system * @return map to retrieve the utility of a competency */ - private Map computeUtilities(Set competencies, RecommendationState state) { + private Map computeUtilities(Set competencies, RecommendationState state) { Map utilities = new HashMap<>(); for (var competency : competencies) { utilities.put(competency.getId(), computeUtilityOfCompetency(competency, state)); @@ -371,7 +371,7 @@ private Map computeUtilities(Set competencies, Recomme * @param state the current state of the recommendation system * @return the utility of the given competency */ - private double computeUtilityOfCompetency(Competency competency, RecommendationState state) { + private double computeUtilityOfCompetency(CourseCompetency competency, RecommendationState state) { // if competency is already mastered there competency has no utility if (state.masteredCompetencies.contains(competency.getId())) { return 0; @@ -390,7 +390,7 @@ private double computeUtilityOfCompetency(Competency competency, RecommendationS * @param competency the competency for which the utility should be computed * @return due date utility of the competency */ - private static double computeDueDateUtility(Competency competency) { + private static double computeDueDateUtility(CourseCompetency competency) { final var earliestDueDate = getEarliestDueDate(competency); if (earliestDueDate.isEmpty()) { return 0; @@ -416,7 +416,7 @@ else if (timeDelta > 0) { * @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) { + private static Optional getEarliestDueDate(CourseCompetency 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()); @@ -429,7 +429,7 @@ private static Optional getEarliestDueDate(Competency competency) * @param state the current state of the recommendation system * @return prior utility of the competency */ - private static double computePriorUtility(Competency competency, RecommendationState state) { + private static double computePriorUtility(CourseCompetency competency, RecommendationState state) { // return max utility if no prior competencies are present if (state.priorCompetencies.get(competency.getId()).isEmpty()) { return PRIOR_UTILITY; @@ -447,7 +447,7 @@ private static double computePriorUtility(Competency competency, RecommendationS * @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) { + private static double computeExtendsOrAssumesUtility(CourseCompetency 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) { @@ -464,7 +464,7 @@ private static double computeExtendsOrAssumesUtility(Competency competency, Reco * @param state the current state of the recommendation system * @return mastery utility of the competency */ - private static double computeMasteryUtility(Competency competency, RecommendationState state) { + private static double computeMasteryUtility(CourseCompetency competency, RecommendationState state) { return state.competencyMastery.get(competency.getId()) * MASTERY_PROGRESS_UTILITY; } @@ -476,7 +476,7 @@ private static double computeMasteryUtility(Competency competency, Recommendatio * @param state the current state of the recommendation * @return the recommended ordering of learning objects */ - public List getRecommendedOrderOfLearningObjects(User user, Competency competency, RecommendationState state) { + public List getRecommendedOrderOfLearningObjects(User user, CourseCompetency competency, RecommendationState state) { final var combinedPriorConfidence = computeCombinedPriorConfidence(competency, state); return getRecommendedOrderOfLearningObjects(user, competency, combinedPriorConfidence); } @@ -489,7 +489,7 @@ public List getRecommendedOrderOfLearningObjects(User user, Comp * @param combinedPriorConfidence the combined confidence of the user for the prior competencies * @return the recommended ordering of learning objects */ - public List getRecommendedOrderOfLearningObjects(User user, Competency competency, double combinedPriorConfidence) { + public List getRecommendedOrderOfLearningObjects(User user, CourseCompetency competency, double combinedPriorConfidence) { var pendingLectureUnits = competency.getLectureUnits().stream().filter(lectureUnit -> !lectureUnit.isCompletedFor(user)).toList(); List recommendedOrder = new ArrayList<>(pendingLectureUnits); @@ -596,7 +596,7 @@ private static double selectExercisesWithDifficulty(Map c.getUserProgress().stream()) .mapToDouble(CompetencyProgress::getConfidence).sorted().average().orElse(1); } @@ -619,7 +619,7 @@ private static double computeCombinedPriorConfidence(Competency competency, Reco * @param weightedConfidence the weighted confidence of the current and prior competencies * @return the predicted number of exercise points required to master the given competency */ - private double calculateNumberOfExercisePointsRequiredToMaster(User user, Competency competency, double weightedConfidence) { + private double calculateNumberOfExercisePointsRequiredToMaster(User user, CourseCompetency competency, double weightedConfidence) { // we assume that the student may perform slightly worse than previously and dampen the confidence for the prediction process weightedConfidence *= 0.9; double currentPoints = participantScoreService.getStudentAndTeamParticipationPointsAsDoubleStream(user, competency.getExercises()).sum(); @@ -690,7 +690,7 @@ private static double[] getExerciseDifficultyDistribution(double weightedConfide return EXERCISE_DIFFICULTY_DISTRIBUTION_LUT[Math.clamp(distributionIndex, 0, EXERCISE_DIFFICULTY_DISTRIBUTION_LUT.length - 1)]; } - public record RecommendationState(Map competencyIdMap, List recommendedOrderOfCompetencies, Set masteredCompetencies, + public record RecommendationState(Map competencyIdMap, List recommendedOrderOfCompetencies, Set masteredCompetencies, Map competencyMastery, Map> matchingClusters, Map> priorCompetencies, Map extendsCompetencies, Map assumesCompetencies) { } @@ -704,7 +704,7 @@ public record RecommendationState(Map competencyIdMap, List getOrderOfLearningObjectsForCompetency(long competencyId, User user) { - Competency competency = competencyRepository.findByIdWithExercisesAndLectureUnitsElseThrow(competencyId); + CourseCompetency competency = courseCompetencyRepository.findByIdWithExercisesAndLectureUnitsElseThrow(competencyId); return getOrderOfLearningObjectsForCompetency(competency, user); } @@ -716,7 +716,7 @@ public List getOrderOfLearningObjectsForCompetency(long competen * @param user the user for which the recommendation should be generated * @return the recommended order of learning objects */ - public List getOrderOfLearningObjectsForCompetency(Competency competency, User user) { + public List getOrderOfLearningObjectsForCompetency(CourseCompetency competency, User user) { Optional optionalCompetencyProgress = competencyProgressRepository.findByCompetencyIdAndUserId(competency.getId(), user.getId()); competency.setUserProgress(optionalCompetencyProgress.map(Set::of).orElse(Set.of())); learningObjectService.setLectureUnitCompletions(competency.getLectureUnits(), user); diff --git a/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathService.java index f091e76e77d8..1ff89ddaa23d 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathService.java @@ -25,6 +25,7 @@ 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.CourseCompetency; import de.tum.in.www1.artemis.domain.competency.LearningPath; import de.tum.in.www1.artemis.domain.lecture.ExerciseUnit; import de.tum.in.www1.artemis.domain.lecture.LectureUnit; @@ -146,6 +147,7 @@ public LearningPath generateLearningPathForUser(@NotNull Course course, @NotNull lpToCreate.setUser(user); lpToCreate.setCourse(course); lpToCreate.getCompetencies().addAll(course.getCompetencies()); + lpToCreate.getCompetencies().addAll(course.getPrerequisites()); var persistedLearningPath = learningPathRepository.save(lpToCreate); log.debug("Created LearningPath (id={}) for user (id={}) in course (id={})", persistedLearningPath.getId(), user.getId(), course.getId()); updateLearningPathProgress(persistedLearningPath); @@ -230,7 +232,7 @@ public void updateLearningPathProgress(long courseId, long userId) { */ 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 competencyIds = learningPath.getCompetencies().stream().map(CourseCompetency::getId).collect(Collectors.toSet()); final var competencyProgresses = competencyProgressRepository.findAllByCompetencyIdsAndUserId(competencyIds, userId); final float completed = competencyProgresses.stream().filter(CompetencyProgressService::isMastered).count(); @@ -304,8 +306,8 @@ private void checkNoRelations(@NotNull Course course, @NotNull Set competencies = competencyRepository.findAllByLearningPath(learningPath); - Set competencyIds = competencies.stream().map(Competency::getId).collect(Collectors.toSet()); + Set competencies = learningPath.getCompetencies(); + Set competencyIds = competencies.stream().map(CourseCompetency::getId).collect(Collectors.toSet()); Map competencyProgresses = competencyProgressRepository.findAllByCompetencyIdsAndUserId(competencyIds, user.getId()).stream() .collect(Collectors.toMap(progress -> progress.getCompetency().getId(), cp -> cp)); @@ -395,7 +397,7 @@ public LearningPath findWithCompetenciesAndLearningObjectsAndCompletedUsersById( return learningPath; } Long userId = learningPath.getUser().getId(); - Set competencyIds = learningPath.getCompetencies().stream().map(Competency::getId).collect(Collectors.toSet()); + Set competencyIds = learningPath.getCompetencies().stream().map(CourseCompetency::getId).collect(Collectors.toSet()); Map competencyProgresses = competencyProgressRepository.findAllByCompetencyIdsAndUserId(competencyIds, userId).stream() .collect(Collectors.toMap(progress -> progress.getCompetency().getId(), cp -> cp)); Set lectureUnits = learningPath.getCompetencies().stream().flatMap(competency -> competency.getLectureUnits().stream()).collect(Collectors.toSet()); 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 8a73af572b16..c366793de8ac 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 @@ -336,7 +336,7 @@ else if (courseUpdate.getCourseIcon() == null && existingCourse.getCourseIcon() // if learning paths got enabled, generate learning paths for students if (existingCourse.getLearningPathsEnabled() != courseUpdate.getLearningPathsEnabled() && courseUpdate.getLearningPathsEnabled()) { - Course courseWithCompetencies = courseRepository.findWithEagerCompetenciesByIdElseThrow(result.getId()); + Course courseWithCompetencies = courseRepository.findWithEagerCompetenciesAndPrerequisitesByIdElseThrow(result.getId()); learningPathService.generateLearningPaths(courseWithCompetencies); } @@ -439,8 +439,8 @@ public ResponseEntity> findAllOnlineCoursesForLtiDashboard @PostMapping("courses/{courseId}/enroll") @EnforceAtLeastStudent public ResponseEntity> enrollInCourse(@PathVariable Long courseId) { - Course course = courseRepository.findWithEagerOrganizationsAndCompetenciesAndLearningPathsElseThrow(courseId); User user = userRepository.getUserWithGroupsAndAuthoritiesAndOrganizations(); + Course course = courseRepository.findWithEagerOrganizationsAndCompetenciesAndPrerequisitesAndLearningPathsElseThrow(courseId); log.debug("REST request to enroll {} in Course {}", user.getName(), course.getTitle()); courseService.enrollUserForCourseOrThrow(user, course); return ResponseEntity.ok(user.getGroups()); @@ -1251,7 +1251,7 @@ public ResponseEntity addUserToCourseGroup(String userLogin, User instruct } courseService.addUserToGroup(userToAddToGroup.get(), group); if (role == Role.STUDENT && course.getLearningPathsEnabled()) { - Course courseWithCompetencies = courseRepository.findWithEagerCompetenciesByIdElseThrow(course.getId()); + Course courseWithCompetencies = courseRepository.findWithEagerCompetenciesAndPrerequisitesByIdElseThrow(course.getId()); learningPathService.generateLearningPathForUser(courseWithCompetencies, userToAddToGroup.get()); } return ResponseEntity.ok().body(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 1b96a70d16b9..6e6d787f78e7 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 @@ -105,7 +105,7 @@ public LearningPathResource(CourseService courseService, CourseRepository course @EnforceAtLeastInstructorInCourse public ResponseEntity enableLearningPathsForCourse(@PathVariable long courseId) { log.debug("REST request to enable learning paths for course with id: {}", courseId); - Course course = courseRepository.findWithEagerCompetenciesByIdElseThrow(courseId); + Course course = courseRepository.findWithEagerCompetenciesAndPrerequisitesByIdElseThrow(courseId); if (course.getLearningPathsEnabled()) { throw new BadRequestException("Learning paths are already enabled for this course."); } @@ -126,7 +126,7 @@ public ResponseEntity enableLearningPathsForCourse(@PathVariable long cour @EnforceAtLeastInstructorInCourse 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); + Course course = courseRepository.findWithEagerCompetenciesAndPrerequisitesByIdElseThrow(courseId); courseService.checkLearningPathsEnabledElseThrow(course); learningPathService.generateLearningPaths(course); return ResponseEntity.ok().build(); @@ -313,7 +313,7 @@ public ResponseEntity generateLearningPath(@PathVariable long courseId) th throw new BadRequestException("Learning path already exists."); } - final var course = courseRepository.findWithEagerCompetenciesByIdElseThrow(courseId); + final var course = courseRepository.findWithEagerCompetenciesAndPrerequisitesByIdElseThrow(courseId); final var learningPath = learningPathService.generateLearningPathForUser(course, user); return ResponseEntity.created(new URI("api/learning-path/" + learningPath.getId())).body(learningPath.getId()); } 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/competency/CompetencyResource.java similarity index 96% rename from src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java rename to src/main/java/de/tum/in/www1/artemis/web/rest/competency/CompetencyResource.java index aa33825f2de7..96d294e74a41 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/competency/CompetencyResource.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.web.rest; +package de.tum.in.www1.artemis.web.rest.competency; import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; @@ -36,7 +36,7 @@ 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.CompetencyProgress; -import de.tum.in.www1.artemis.domain.competency.CompetencyRelation; +import de.tum.in.www1.artemis.domain.competency.CourseCompetency; import de.tum.in.www1.artemis.repository.CompetencyProgressRepository; import de.tum.in.www1.artemis.repository.CompetencyRelationRepository; import de.tum.in.www1.artemis.repository.CompetencyRepository; @@ -242,7 +242,7 @@ public ResponseEntity createCompetency(@PathVariable long courseId, if (competency.getId() != null || competency.getTitle() == null || competency.getTitle().trim().isEmpty()) { throw new BadRequestException(); } - var course = courseRepository.findWithEagerCompetenciesByIdElseThrow(courseId); + var course = courseRepository.findWithEagerCompetenciesAndPrerequisitesByIdElseThrow(courseId); final var persistedCompetency = competencyService.createCompetency(competency, course); @@ -266,7 +266,7 @@ public ResponseEntity> createCompetencies(@PathVariable Long co throw new BadRequestException(); } } - var course = courseRepository.findWithEagerCompetenciesByIdElseThrow(courseId); + var course = courseRepository.findWithEagerCompetenciesAndPrerequisitesByIdElseThrow(courseId); var createdCompetencies = competencyService.createCompetencies(competencies, course); @@ -286,7 +286,7 @@ public ResponseEntity> createCompetencies(@PathVariable Long co public ResponseEntity importCompetency(@PathVariable long courseId, @RequestBody Competency competencyToImport) throws URISyntaxException { log.info("REST request to import a competency: {}", competencyToImport.getId()); - var course = courseRepository.findWithEagerCompetenciesByIdElseThrow(courseId); + var course = courseRepository.findWithEagerCompetenciesAndPrerequisitesByIdElseThrow(courseId); authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, competencyToImport.getCourse(), null); if (competencyToImport.getCourse().getId().equals(courseId)) { @@ -313,7 +313,7 @@ public ResponseEntity> importCompetencies(@P @RequestParam(defaultValue = "false") boolean importRelations) throws URISyntaxException { log.info("REST request to import competencies: {}", competenciesToImport); - var course = courseRepository.findWithEagerCompetenciesByIdElseThrow(courseId); + var course = courseRepository.findWithEagerCompetenciesAndPrerequisitesByIdElseThrow(courseId); var competencies = new HashSet<>(competenciesToImport); List importedCompetencies; @@ -482,20 +482,20 @@ public ResponseEntity> getCompetencyRelations(@PathVa */ @PostMapping("courses/{courseId}/competencies/relations") @EnforceAtLeastInstructorInCourse - public ResponseEntity createCompetencyRelation(@PathVariable long courseId, @RequestBody CompetencyRelation relation) { - var tailId = relation.getTailCompetency().getId(); - var headId = relation.getHeadCompetency().getId(); + public ResponseEntity createCompetencyRelation(@PathVariable long courseId, @RequestBody CompetencyRelationDTO relation) { + var tailId = relation.tailCompetencyId(); + var headId = relation.headCompetencyId(); log.info("REST request to create a relation between competencies {} and {}", tailId, headId); var course = courseRepository.findByIdElseThrow(courseId); - var tailCompetency = competencyRepository.findByIdElseThrow(tailId); + var tailCompetency = courseCompetencyRepository.findByIdElseThrow(tailId); checkAuthorizationForCompetency(Role.INSTRUCTOR, course, tailCompetency); - var headCompetency = competencyRepository.findByIdElseThrow(headId); + var headCompetency = courseCompetencyRepository.findByIdElseThrow(headId); checkAuthorizationForCompetency(Role.INSTRUCTOR, course, headCompetency); - var createdRelation = competencyRelationService.createCompetencyRelation(tailCompetency, headCompetency, relation.getType(), course); + var createdRelation = competencyRelationService.createCompetencyRelation(tailCompetency, headCompetency, relation.relationType(), course); - return ResponseEntity.ok().body(createdRelation); + return ResponseEntity.ok().body(CompetencyRelationDTO.of(createdRelation)); } /** @@ -561,7 +561,7 @@ public ResponseEntity> getCourseCompetencyTitles(@PathVariable Long * @param course The course for which to check the authorization role for * @param competency The competency to be accessed by the user */ - private void checkAuthorizationForCompetency(Role role, @NotNull Course course, @NotNull Competency competency) { + private void checkAuthorizationForCompetency(Role role, @NotNull Course course, @NotNull CourseCompetency competency) { if (competency.getCourse() == null) { throw new BadRequestAlertException("A competency must belong to a course", ENTITY_NAME, "competencyNoCourse"); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/competency/CourseCompetencyResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/competency/CourseCompetencyResource.java new file mode 100644 index 000000000000..bd2fc6cfd004 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/competency/CourseCompetencyResource.java @@ -0,0 +1,56 @@ +package de.tum.in.www1.artemis.web.rest.competency; + +import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import de.tum.in.www1.artemis.domain.User; +import de.tum.in.www1.artemis.domain.competency.CourseCompetency; +import de.tum.in.www1.artemis.repository.UserRepository; +import de.tum.in.www1.artemis.security.annotations.enforceRoleInCourse.EnforceAtLeastStudentInCourse; +import de.tum.in.www1.artemis.service.competency.CourseCompetencyService; + +@Profile(PROFILE_CORE) +@RestController +@RequestMapping("api/") +public class CourseCompetencyResource { + + @Value("${jhipster.clientApp.name}") + private String applicationName; + + private static final Logger log = LoggerFactory.getLogger(CourseCompetencyResource.class); + + private final UserRepository userRepository; + + private final CourseCompetencyService courseCompetencyService; + + public CourseCompetencyResource(UserRepository userRepository, CourseCompetencyService courseCompetencyService) { + this.userRepository = userRepository; + this.courseCompetencyService = courseCompetencyService; + } + + /** + * GET courses/:courseId/competencies : gets all the course competencies of a course + * + * @param courseId the id of the course for which the competencies should be fetched + * @return the ResponseEntity with status 200 (OK) and with body the found competencies + */ + @GetMapping("courses/{courseId}/course-competencies") + @EnforceAtLeastStudentInCourse + public ResponseEntity> getCourseCompetenciesWithProgress(@PathVariable long courseId) { + log.debug("REST request to get competencies for course with id: {}", courseId); + User user = userRepository.getUserWithGroupsAndAuthorities(); + final var competencies = courseCompetencyService.findCourseCompetenciesWithProgressForUserByCourseId(courseId, user.getId()); + return ResponseEntity.ok(competencies); + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/CompetencyGraphNodeDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/CompetencyGraphNodeDTO.java index c90603c32fde..91e8917e42fb 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/CompetencyGraphNodeDTO.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/CompetencyGraphNodeDTO.java @@ -7,14 +7,14 @@ import com.fasterxml.jackson.annotation.JsonInclude; -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.CourseCompetency; import de.tum.in.www1.artemis.service.competency.CompetencyProgressService; @JsonInclude(JsonInclude.Include.NON_EMPTY) public record CompetencyGraphNodeDTO(String id, String label, ZonedDateTime softDueDate, Double progress, Double confidence, Double masteryProgress) { - public static CompetencyGraphNodeDTO of(@NotNull Competency competency, @NotNull Optional competencyProgress) { + public static CompetencyGraphNodeDTO of(@NotNull CourseCompetency competency, @NotNull Optional competencyProgress) { return new CompetencyGraphNodeDTO(competency.getId().toString(), competency.getTitle(), competency.getSoftDueDate(), competencyProgress.map(CompetencyProgress::getProgress).orElse(0.0), competencyProgress.map(CompetencyProgress::getConfidence).orElse(1.0), competencyProgress.map(CompetencyProgressService::getMasteryProgress).orElse(0.0)); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/CompetencyNameDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/CompetencyNameDTO.java index 843049872cfb..40f93f4d0c07 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/CompetencyNameDTO.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/CompetencyNameDTO.java @@ -4,14 +4,14 @@ import com.fasterxml.jackson.annotation.JsonInclude; -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.CourseCompetency; import de.tum.in.www1.artemis.service.competency.CompetencyProgressService; @JsonInclude(JsonInclude.Include.NON_EMPTY) public record CompetencyNameDTO(long id, String title, double masteryProgress) { - public static CompetencyNameDTO of(Competency competency) { + public static CompetencyNameDTO of(CourseCompetency competency) { Optional optionalProgress = competency.getUserProgress().stream().findFirst(); return new CompetencyNameDTO(competency.getId(), competency.getTitle(), optionalProgress.map(CompetencyProgressService::getMasteryProgress).orElse(0.0)); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/CompetencyRelationDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/CompetencyRelationDTO.java index 5cbf7ba6054a..8da3815236b1 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/CompetencyRelationDTO.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/CompetencyRelationDTO.java @@ -13,7 +13,7 @@ * @param relationType the relation type */ @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record CompetencyRelationDTO(long id, long tailCompetencyId, long headCompetencyId, RelationType relationType) { +public record CompetencyRelationDTO(Long id, long tailCompetencyId, long headCompetencyId, RelationType relationType) { public static CompetencyRelationDTO of(CompetencyRelation competencyRelation) { return new CompetencyRelationDTO(competencyRelation.getId(), competencyRelation.getTailCompetency().getId(), competencyRelation.getHeadCompetency().getId(), diff --git a/src/main/webapp/app/course/competencies/competency-management/competency-management.component.html b/src/main/webapp/app/course/competencies/competency-management/competency-management.component.html index d2c9bc2fbbaf..ec5b9e8dc1d7 100644 --- a/src/main/webapp/app/course/competencies/competency-management/competency-management.component.html +++ b/src/main/webapp/app/course/competencies/competency-management/competency-management.component.html @@ -47,7 +47,7 @@

} { this.competencies = this.competencies.filter((competency) => competency.id !== competencyId); + this.courseCompetencies = [...this.competencies, ...this.prerequisites]; this.relations = this.relations.filter((relation) => relation.tailCompetency?.id !== competencyId && relation.headCompetency?.id !== competencyId); this.dialogErrorSource.next(''); }, @@ -113,6 +116,7 @@ export class CompetencyManagementComponent implements OnInit, OnDestroy { next: () => { this.alertService.success('artemisApp.prerequisite.manage.deleted'); this.prerequisites = this.prerequisites.filter((prerequisite) => prerequisite.id !== prerequisiteId); + this.courseCompetencies = [...this.competencies, ...this.prerequisites]; this.dialogErrorSource.next(''); }, error: (error: HttpErrorResponse) => onError(this.alertService, error), @@ -160,6 +164,7 @@ export class CompetencyManagementComponent implements OnInit, OnDestroy { forkJoin([relationsObservable, prerequisitesObservable, competencyProgressObservable]).subscribe({ next: ([competencyRelations, prerequisites, competencyProgressResponses]) => { this.prerequisites = prerequisites; + this.courseCompetencies = [...this.competencies, ...this.prerequisites]; this.relations = (competencyRelations.body ?? []).map((relationDTO) => dtoToCompetencyRelation(relationDTO)); for (const competencyProgressResponse of competencyProgressResponses) { @@ -215,6 +220,7 @@ export class CompetencyManagementComponent implements OnInit, OnDestroy { .map((dto) => dtoToCompetencyRelation(dto)); this.competencies = this.competencies.concat(importedCompetencies); + this.courseCompetencies = [...this.competencies, ...this.prerequisites]; this.relations = this.relations.concat(importedRelations); } @@ -224,16 +230,17 @@ export class CompetencyManagementComponent implements OnInit, OnDestroy { * @param relation the given competency relation */ createRelation(relation: CompetencyRelation) { + console.log(relation); this.competencyService .createCompetencyRelation(relation, this.courseId) .pipe( - filter((res: HttpResponse) => res.ok), - map((res: HttpResponse) => res.body), + filter((res) => res.ok), + map((res) => res.body), ) .subscribe({ next: (relation) => { if (relation) { - this.relations = this.relations.concat(relation); + this.relations = this.relations.concat(dtoToCompetencyRelation(relation)); } }, error: (res: HttpErrorResponse) => onError(this.alertService, res), diff --git a/src/main/webapp/app/course/competencies/competency-management/competency-relation-graph.component.ts b/src/main/webapp/app/course/competencies/competency-management/competency-relation-graph.component.ts index bf8295ab7b71..2fc93e4a8e63 100644 --- a/src/main/webapp/app/course/competencies/competency-management/competency-relation-graph.component.ts +++ b/src/main/webapp/app/course/competencies/competency-management/competency-relation-graph.component.ts @@ -1,7 +1,7 @@ import { Component, EventEmitter, Output, computed, input } from '@angular/core'; import { faArrowsToEye } from '@fortawesome/free-solid-svg-icons'; import { Edge, NgxGraphZoomOptions, Node } from '@swimlane/ngx-graph'; -import { Competency, CompetencyRelation, CompetencyRelationError, CompetencyRelationType } from 'app/entities/competency.model'; +import { CompetencyRelation, CompetencyRelationError, CompetencyRelationType, CourseCompetency } from 'app/entities/competency.model'; import { Subject } from 'rxjs'; @Component({ @@ -10,7 +10,7 @@ import { Subject } from 'rxjs'; styleUrls: ['./competency-relation-graph.component.scss'], }) export class CompetencyRelationGraphComponent { - competencies = input([]); + competencies = input([]); relations = input([]); @Output() onRemoveRelation = new EventEmitter(); diff --git a/src/main/webapp/app/course/competencies/competency.service.ts b/src/main/webapp/app/course/competencies/competency.service.ts index b4ba83471401..2f2103f73cfa 100644 --- a/src/main/webapp/app/course/competencies/competency.service.ts +++ b/src/main/webapp/app/course/competencies/competency.service.ts @@ -124,6 +124,7 @@ export class CompetencyService { create(competency: Competency, courseId: number): Observable { const copy = this.convertCompetencyFromClient(competency); + console.log(copy); return this.httpClient.post(`${this.resourceURL}/courses/${courseId}/competencies`, copy, { observe: 'response' }); } @@ -173,9 +174,9 @@ export class CompetencyService { } //relations - createCompetencyRelation(relation: CompetencyRelation, courseId: number) { - return this.httpClient.post(`${this.resourceURL}/courses/${courseId}/competencies/relations`, relation, { + const relationDTO: CompetencyRelationDTO = { tailCompetencyId: relation.tailCompetency?.id, headCompetencyId: relation.headCompetency?.id, relationType: relation.type }; + return this.httpClient.post(`${this.resourceURL}/courses/${courseId}/competencies/relations`, relationDTO, { observe: 'response', }); } diff --git a/src/main/webapp/app/course/competencies/course-competency.service.ts b/src/main/webapp/app/course/competencies/course-competency.service.ts new file mode 100644 index 000000000000..96c813fcc547 --- /dev/null +++ b/src/main/webapp/app/course/competencies/course-competency.service.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { Competency, CourseCompetency } from 'app/entities/competency.model'; +import { map, tap } from 'rxjs/operators'; +import { EntityTitleService, EntityType } from 'app/shared/layouts/navbar/entity-title.service'; +import { convertDateFromServer } from 'app/utils/date.utils'; + +type EntityArrayResponseType = HttpResponse; + +@Injectable({ + providedIn: 'root', +}) +export class CourseCompetencyService { + private resourceURL = 'api'; + + constructor( + private httpClient: HttpClient, + private entityTitleService: EntityTitleService, + ) {} + + getAllForCourse(courseId: number): Observable { + return this.httpClient.get(`${this.resourceURL}/courses/${courseId}/course-competencies`, { observe: 'response' }).pipe( + map((res: EntityArrayResponseType) => CourseCompetencyService.convertArrayResponseDatesFromServer(res)), + tap((res: EntityArrayResponseType) => res?.body?.forEach(this.sendTitlesToEntityTitleService.bind(this))), + ); + } + + /** + * Helper methods for date conversion from server and client + */ + private static convertArrayResponseDatesFromServer(res: EntityArrayResponseType): EntityArrayResponseType { + if (res.body) { + res.body.map((competency: Competency) => (competency.softDueDate = convertDateFromServer(competency.softDueDate))); + } + return res; + } + + private sendTitlesToEntityTitleService(competency: Competency | undefined | null) { + this.entityTitleService.setTitle(EntityType.COMPETENCY, [competency?.id], competency?.title); + } +} diff --git a/src/main/webapp/app/course/competencies/create-competency/create-competency.component.ts b/src/main/webapp/app/course/competencies/create-competency/create-competency.component.ts index c250e296fbd5..b5703dc67b74 100644 --- a/src/main/webapp/app/course/competencies/create-competency/create-competency.component.ts +++ b/src/main/webapp/app/course/competencies/create-competency/create-competency.component.ts @@ -18,7 +18,7 @@ import { DocumentationType } from 'app/shared/components/documentation-button/do }) export class CreateCompetencyComponent implements OnInit { readonly documentationType: DocumentationType = 'Competencies'; - competencyToCreate: Competency = {}; + competencyToCreate: Competency = new Competency(); isLoading: boolean; courseId: number; lecturesWithLectureUnits: Lecture[] = []; diff --git a/src/main/webapp/app/entities/competency.model.ts b/src/main/webapp/app/entities/competency.model.ts index 4b3165a47d06..c40c4c896f03 100644 --- a/src/main/webapp/app/entities/competency.model.ts +++ b/src/main/webapp/app/entities/competency.model.ts @@ -39,28 +39,45 @@ export enum CourseCompetencyValidators { MASTERY_THRESHOLD_MAX = 100, } +// IMPORTANT NOTICE: The following strings have to be consistent with the ones defined in CourseCompetency.java +export enum CourseCompetencyType { + COMPETENCY = 'competency', + PREREQUISITE = 'prerequisite', +} + export const DEFAULT_MASTERY_THRESHOLD = 100; -export interface BaseCompetency extends BaseEntity { +export abstract class BaseCompetency implements BaseEntity { + id?: number; title?: string; description?: string; taxonomy?: CompetencyTaxonomy; } -export interface CourseCompetency extends BaseCompetency { +export abstract class CourseCompetency extends BaseCompetency { softDueDate?: dayjs.Dayjs; masteryThreshold?: number; optional?: boolean; - course?: Course; - linkedCourseCompetency?: CourseCompetency; -} - -export interface Competency extends CourseCompetency { + linkedStandardizedCompetency?: StandardizedCompetency; exercises?: Exercise[]; lectureUnits?: LectureUnit[]; userProgress?: CompetencyProgress[]; courseProgress?: CourseCompetencyProgress; - linkedStandardizedCompetency?: StandardizedCompetency; + course?: Course; + linkedCourseCompetency?: CourseCompetency; + + public type?: CourseCompetencyType; + + protected constructor(type: CourseCompetencyType) { + super(); + this.type = type; + } +} + +export class Competency extends CourseCompetency { + constructor() { + super(CourseCompetencyType.COMPETENCY); + } } export class CompetencyJol { @@ -146,8 +163,8 @@ export class CourseCompetencyProgress { export class CompetencyRelation implements BaseEntity { public id?: number; - public tailCompetency?: Competency; - public headCompetency?: Competency; + public tailCompetency?: CourseCompetency; + public headCompetency?: CourseCompetency; public type?: CompetencyRelationType; } 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 eb957c80ca51..d2ad12be1a3f 100644 --- a/src/main/webapp/app/entities/competency/learning-path.model.ts +++ b/src/main/webapp/app/entities/competency/learning-path.model.ts @@ -1,7 +1,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, CompetencyRelationType } from 'app/entities/competency.model'; +import { CompetencyRelationType, CourseCompetency } from 'app/entities/competency.model'; import { Edge, Node, NodeDimension } from '@swimlane/ngx-graph'; import { faCheckCircle, faCircle, faFlag, faFlagCheckered, faPlayCircle, faSignsPost } from '@fortawesome/free-solid-svg-icons'; @@ -10,7 +10,7 @@ export class LearningPath implements BaseEntity { public progress?: number; public user?: User; public course?: Course; - public competencies?: Competency[]; + public competencies?: CourseCompetency[]; constructor() {} } diff --git a/src/main/webapp/app/entities/competency/standardized-competency.model.ts b/src/main/webapp/app/entities/competency/standardized-competency.model.ts index 303098d12abd..6db4767cd116 100644 --- a/src/main/webapp/app/entities/competency/standardized-competency.model.ts +++ b/src/main/webapp/app/entities/competency/standardized-competency.model.ts @@ -1,4 +1,4 @@ -import { Competency, CompetencyTaxonomy } from 'app/entities/competency.model'; +import { CompetencyTaxonomy, CourseCompetency } from 'app/entities/competency.model'; import { BaseEntity } from 'app/shared/model/base-entity'; import { BaseCompetency } from 'app/entities/competency.model'; @@ -8,7 +8,7 @@ export interface StandardizedCompetency extends BaseCompetency { source?: Source; firstVersion?: StandardizedCompetency; childVersions?: StandardizedCompetency[]; - linkedCompetencies?: Competency[]; + linkedCompetencies?: CourseCompetency[]; } export interface StandardizedCompetencyDTO extends BaseEntity { diff --git a/src/main/webapp/app/entities/exercise.model.ts b/src/main/webapp/app/entities/exercise.model.ts index d195aba6bbdb..fa285e0a6e60 100644 --- a/src/main/webapp/app/entities/exercise.model.ts +++ b/src/main/webapp/app/entities/exercise.model.ts @@ -12,7 +12,7 @@ import { GradingCriterion } from 'app/exercises/shared/structured-grading-criter import { Team } from 'app/entities/team.model'; import { DueDateStat } from 'app/course/dashboards/due-date-stat.model'; import { ExerciseGroup } from 'app/entities/exercise-group.model'; -import { Competency } from 'app/entities/competency.model'; +import { CourseCompetency } from 'app/entities/competency.model'; import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { ExerciseCategory } from 'app/entities/exercise-category.model'; import { ExerciseInfo } from 'app/exam/exam-scores/exam-score-dtos.model'; @@ -105,7 +105,7 @@ export abstract class Exercise implements BaseEntity { public posts?: Post[]; public gradingCriteria?: GradingCriterion[]; public exerciseGroup?: ExerciseGroup; - public competencies?: Competency[]; + public competencies?: CourseCompetency[]; public plagiarismDetectionConfig?: PlagiarismDetectionConfig = DEFAULT_PLAGIARISM_DETECTION_CONFIG; // default value 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 48863fff879d..fa16b33e5119 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,7 @@ 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 { CourseCompetency } from 'app/entities/competency.model'; import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { faDownload, faLink, faQuestion, faScroll, faVideo } from '@fortawesome/free-solid-svg-icons'; @@ -36,7 +36,7 @@ export abstract class LectureUnit implements BaseEntity { public name?: string; public releaseDate?: dayjs.Dayjs; public lecture?: Lecture; - public competencies?: Competency[]; + public competencies?: CourseCompetency[]; public type?: LectureUnitType; // calculated property public visibleToStudents?: boolean; diff --git a/src/main/webapp/app/entities/prerequisite.model.ts b/src/main/webapp/app/entities/prerequisite.model.ts index aa47bc7c31bd..d96567316b22 100644 --- a/src/main/webapp/app/entities/prerequisite.model.ts +++ b/src/main/webapp/app/entities/prerequisite.model.ts @@ -1,6 +1,10 @@ -import { CompetencyTaxonomy, CourseCompetency } from 'app/entities/competency.model'; +import { CompetencyTaxonomy, CourseCompetency, CourseCompetencyType } from 'app/entities/competency.model'; -export interface Prerequisite extends CourseCompetency {} +export class Prerequisite extends CourseCompetency { + constructor() { + super(CourseCompetencyType.PREREQUISITE); + } +} export interface PrerequisiteResponseDTO extends Omit { linkedCourseCompetencyDTO?: LinkedCourseCompetencyDTO; diff --git a/src/main/webapp/app/shared/competency-selection/competency-selection.component.ts b/src/main/webapp/app/shared/competency-selection/competency-selection.component.ts index c145611a9620..50febc424a84 100644 --- a/src/main/webapp/app/shared/competency-selection/competency-selection.component.ts +++ b/src/main/webapp/app/shared/competency-selection/competency-selection.component.ts @@ -1,11 +1,11 @@ import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, forwardRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; -import { Competency, getIcon } from 'app/entities/competency.model'; -import { CompetencyService } from 'app/course/competencies/competency.service'; +import { Competency, CourseCompetency, getIcon } from 'app/entities/competency.model'; import { ActivatedRoute } from '@angular/router'; import { CourseStorageService } from 'app/course/manage/course-storage.service'; import { finalize } from 'rxjs'; +import { CourseCompetencyService } from 'app/course/competencies/course-competency.service'; @Component({ selector: 'jhi-competency-selection', @@ -22,14 +22,15 @@ import { finalize } from 'rxjs'; export class CompetencySelectionComponent implements OnInit, ControlValueAccessor { @Input() labelName: string; @Input() labelTooltip: string; - // selected competencies - @Input() value?: Competency[]; - @Input() disabled: boolean; - // all course competencies - @Input() competencies?: Competency[]; @Output() valueChange = new EventEmitter(); + disabled: boolean; + // selected competencies + selectedCompetencies?: Competency[]; + // all course competencies + competencies?: CourseCompetency[]; + isLoading = false; checkboxStates: Record; @@ -42,7 +43,7 @@ export class CompetencySelectionComponent implements OnInit, ControlValueAccesso constructor( private route: ActivatedRoute, private courseStorageService: CourseStorageService, - private competencyService: CompetencyService, + private courseCompetencyService: CourseCompetencyService, private changeDetector: ChangeDetectorRef, ) {} @@ -51,11 +52,11 @@ export class CompetencySelectionComponent implements OnInit, ControlValueAccesso if (!this.competencies && courseId) { const course = this.courseStorageService.getCourse(courseId); // an empty array is used as fallback, if a course is cached, where no competencies have been queried - if (course?.competencies?.length) { - this.setCompetencies(course.competencies!); + if (course?.competencies?.length || course?.prerequisites?.length) { + this.setCompetencies([...(course.competencies ?? []), ...(course.prerequisites ?? [])]); } else { this.isLoading = true; - this.competencyService + this.courseCompetencyService .getAllForCourse(courseId) .pipe( finalize(() => { @@ -69,7 +70,7 @@ export class CompetencySelectionComponent implements OnInit, ControlValueAccesso .subscribe({ next: (response) => { this.setCompetencies(response.body!); - this.writeValue(this.value); + this.writeValue(this.selectedCompetencies); }, error: () => { this.disabled = true; @@ -83,7 +84,7 @@ export class CompetencySelectionComponent implements OnInit, ControlValueAccesso * Set the available competencies for selection * @param competencies The competencies of the course */ - setCompetencies(competencies: Competency[]) { + setCompetencies(competencies: CourseCompetency[]) { this.competencies = competencies.map((competency) => { // Remove unnecessary properties competency.course = undefined; @@ -93,7 +94,7 @@ export class CompetencySelectionComponent implements OnInit, ControlValueAccesso this.checkboxStates = this.competencies.reduce( (states, competency) => { if (competency.id) { - states[competency.id] = !!this.value?.find((value) => value.id === competency.id); + states[competency.id] = !!this.selectedCompetencies?.find((value) => value.id === competency.id); } return states; }, @@ -104,20 +105,20 @@ export class CompetencySelectionComponent implements OnInit, ControlValueAccesso toggleCompetency(newValue: Competency) { if (newValue.id) { if (this.checkboxStates[newValue.id]) { - this.value = this.value?.filter((value) => value.id !== newValue.id); + this.selectedCompetencies = this.selectedCompetencies?.filter((value) => value.id !== newValue.id); } else { - this.value = [...(this.value ?? []), newValue]; + this.selectedCompetencies = [...(this.selectedCompetencies ?? []), newValue]; } this.checkboxStates[newValue.id] = !this.checkboxStates[newValue.id]; // make sure to do not send an empty list to server - if (!this.value?.length) { - this.value = undefined; + if (!this.selectedCompetencies?.length) { + this.selectedCompetencies = undefined; } - this._onChange(this.value); - this.valueChange.emit(this.value); + this._onChange(this.selectedCompetencies); + this.valueChange.emit(this.selectedCompetencies); } } @@ -125,9 +126,9 @@ export class CompetencySelectionComponent implements OnInit, ControlValueAccesso if (value && this.competencies) { // Compare the ids of the competencies instead of the whole objects const ids = value.map((el) => el.id); - this.value = this.competencies.filter((competency) => ids.includes(competency.id)); + this.selectedCompetencies = this.competencies.filter((competency) => ids.includes(competency.id)); } else { - this.value = value ?? []; + this.selectedCompetencies = value ?? []; } } 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 8d69f512d43f..d63ea676f711 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,9 +7,10 @@ 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.CourseCompetency; import de.tum.in.www1.artemis.domain.competency.LearningPath; import de.tum.in.www1.artemis.repository.CompetencyRepository; +import de.tum.in.www1.artemis.repository.CourseCompetencyRepository; import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.LearningPathRepository; import de.tum.in.www1.artemis.service.learningpath.LearningPathService; @@ -32,6 +33,9 @@ public class LearningPathUtilService { @Autowired private CompetencyRepository competencyRepository; + @Autowired + private CourseCompetencyRepository courseCompetencyRepository; + /** * Enable and generate learning paths for course. * @@ -52,7 +56,7 @@ public Course enableAndGenerateLearningPathsForCourse(Course course) { * @return the persisted learning path */ public LearningPath createLearningPathInCourse(Course course) { - final var competencies = competencyRepository.findAllForCourse(course.getId()); + final var competencies = courseCompetencyRepository.findAllForCourse(course.getId()); LearningPath learningPath = createLearningPath(competencies); learningPath.setCourse(course); return learningPathRepository.save(learningPath); @@ -76,7 +80,7 @@ public LearningPath createLearningPathInCourseForUser(Course course, User user) * @param competencies the competencies that will be linked to the learning path * @return the persisted learning path */ - public LearningPath createLearningPath(Set competencies) { + public LearningPath createLearningPath(Set competencies) { LearningPath learningPath = new LearningPath(); learningPath.setCompetencies(competencies); return learningPathRepository.save(learningPath); 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 f9d1ad5c5737..ab413d2d0c65 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 @@ -64,6 +64,7 @@ import de.tum.in.www1.artemis.repository.AttachmentUnitRepository; import de.tum.in.www1.artemis.repository.CompetencyRelationRepository; import de.tum.in.www1.artemis.repository.CompetencyRepository; +import de.tum.in.www1.artemis.repository.CourseCompetencyRepository; import de.tum.in.www1.artemis.repository.ExerciseRepository; import de.tum.in.www1.artemis.repository.ExerciseUnitRepository; import de.tum.in.www1.artemis.repository.LectureRepository; @@ -163,6 +164,9 @@ class CompetencyIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCT @Autowired private StudentScoreUtilService studentScoreUtilService; + @Autowired + private CourseCompetencyRepository courseCompetencyRepository; + private Course course; private Course course2; @@ -262,7 +266,7 @@ private Lecture createLecture(Course course) { return lecture; } - private TextExercise createTextExercise(ZonedDateTime releaseDate, ZonedDateTime dueDate, ZonedDateTime assassmentDueDate, Set competencies, + private TextExercise createTextExercise(ZonedDateTime releaseDate, ZonedDateTime dueDate, ZonedDateTime assassmentDueDate, Set competencies, boolean isTeamExercise) { // creating text exercise with Result TextExercise textExercise = TextExerciseFactory.generateTextExercise(releaseDate, dueDate, assassmentDueDate, course); @@ -278,7 +282,7 @@ private TextExercise createTextExercise(ZonedDateTime releaseDate, ZonedDateTime return exerciseRepository.save(textExercise); } - private ProgrammingExercise createProgrammingExercise(int i, Set competencies) { + private ProgrammingExercise createProgrammingExercise(int i, Set competencies) { ProgrammingExercise programmingExercise = ProgrammingExerciseFactory.generateProgrammingExercise(null, null, course); programmingExercise.setMaxPoints(i * 10.0); @@ -1096,7 +1100,7 @@ class GetCourseCompetencyTitles { @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") void shouldGetCourseCompetencyTitles() throws Exception { competencyUtilService.createCompetencies(course, 5); - var competencyTitles = competencyRepository.findAllForCourse(course.getId()).stream().map(CourseCompetency::getTitle).toList(); + var competencyTitles = courseCompetencyRepository.findAllForCourse(course.getId()).stream().map(CourseCompetency::getTitle).toList(); prerequisiteUtilService.createPrerequisites(course, 5); var prerequisiteTitles = prerequisiteRepository.findAllByCourseIdOrderById(course.getId()).stream().map(CourseCompetency::getTitle).toList(); var expectedTitles = Stream.concat(competencyTitles.stream(), prerequisiteTitles.stream()).toList(); 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 fb3f1d3074d9..96e135e5cc71 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 @@ -23,7 +23,7 @@ import de.tum.in.www1.artemis.domain.Lecture; import de.tum.in.www1.artemis.domain.TextExercise; 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.CourseCompetency; import de.tum.in.www1.artemis.domain.lecture.AttachmentUnit; import de.tum.in.www1.artemis.domain.lecture.ExerciseUnit; import de.tum.in.www1.artemis.domain.lecture.LectureUnit; @@ -170,7 +170,7 @@ public List createCoursesWithExercisesAndLecturesAndLectureUnits(String * @param competencies The Competencies to add to the LectureUnits * @return The Lecture with updated LectureUnits */ - public Lecture addCompetencyToLectureUnits(Lecture lecture, Set competencies) { + public Lecture addCompetencyToLectureUnits(Lecture lecture, Set competencies) { Lecture l = lectureRepo.findByIdWithLectureUnitsAndCompetenciesElseThrow(lecture.getId()); l.getLectureUnits().forEach(lectureUnit -> { lectureUnit.setCompetencies(competencies); diff --git a/src/test/java/de/tum/in/www1/artemis/service/LearningObjectServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/LearningObjectServiceTest.java index 5044d707a687..eb9b439e6ca3 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/LearningObjectServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/LearningObjectServiceTest.java @@ -18,7 +18,7 @@ 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.CourseCompetency; import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.lecture.LectureFactory; import de.tum.in.www1.artemis.lecture.LectureUtilService; @@ -91,7 +91,7 @@ public Long getId() { } @Override - public Set getCompetencies() { + public Set getCompetencies() { return Set.of(); } }; diff --git a/src/test/java/de/tum/in/www1/artemis/tutorialgroups/AbstractTutorialGroupIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/tutorialgroups/AbstractTutorialGroupIntegrationTest.java index d009bdb2a68e..e44d7aebf9fb 100644 --- a/src/test/java/de/tum/in/www1/artemis/tutorialgroups/AbstractTutorialGroupIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/tutorialgroups/AbstractTutorialGroupIntegrationTest.java @@ -244,7 +244,7 @@ TutorialGroup buildAndSaveTutorialGroupWithoutSchedule(String tutorLogin, String } TutorialGroup buildTutorialGroupWithoutSchedule(String tutorLogin) { - var course = courseRepository.findWithEagerCompetenciesById(exampleCourseId).orElseThrow(); + var course = courseRepository.findByIdElseThrow(exampleCourseId); var tutorialGroup = new TutorialGroup(); tutorialGroup.setCourse(course); tutorialGroup.setTitle(generateRandomTitle()); @@ -253,7 +253,7 @@ TutorialGroup buildTutorialGroupWithoutSchedule(String tutorLogin) { } TutorialGroup buildTutorialGroupWithExampleSchedule(LocalDate validFromInclusive, LocalDate validToInclusive, String tutorLogin) { - var course = courseRepository.findWithEagerCompetenciesById(exampleCourseId).orElseThrow(); + var course = courseRepository.findByIdElseThrow(exampleCourseId); var newTutorialGroup = new TutorialGroup(); newTutorialGroup.setCourse(course); newTutorialGroup.setTitle(generateRandomTitle()); From 595fb643c0c2dd5b3ce506902e7fed309f8ded4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20St=C3=B6hr?= Date: Tue, 2 Jul 2024 17:38:55 +0200 Subject: [PATCH 02/11] Fix tests --- .../artemis/domain/competency/Competency.java | 5 +++++ .../domain/competency/CourseCompetency.java | 2 ++ .../artemis/domain/competency/Prerequisite.java | 5 +++++ .../www1/artemis/repository/CourseRepository.java | 8 ++++---- .../service/learningpath/LearningPathService.java | 6 +++--- .../competency/LearningPathIntegrationTest.java | 4 ++-- .../competency/LearningPathUtilService.java | 2 +- .../lecture/CompetencyIntegrationTest.java | 15 ++++++++++----- 8 files changed, 32 insertions(+), 15 deletions(-) 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 a92d82758fb6..d5e585fb784d 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 @@ -15,4 +15,9 @@ public Competency(String title, String description, ZonedDateTime softDueDate, I public Competency() { } + + @Override + public String getType() { + return "competency"; + } } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/competency/CourseCompetency.java b/src/main/java/de/tum/in/www1/artemis/domain/competency/CourseCompetency.java index 28ba820d3ac3..686e1d56b53a 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/competency/CourseCompetency.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/competency/CourseCompetency.java @@ -100,6 +100,8 @@ public abstract class CourseCompetency extends BaseCompetency { @OneToMany(mappedBy = "competency", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) private Set competencyJols = new HashSet<>(); + public abstract String getType(); + public CourseCompetency() { } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/competency/Prerequisite.java b/src/main/java/de/tum/in/www1/artemis/domain/competency/Prerequisite.java index a8f7c214e170..cb607e587a5d 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/competency/Prerequisite.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/competency/Prerequisite.java @@ -18,4 +18,9 @@ public Prerequisite(String title, String description, ZonedDateTime softDueDate, public Prerequisite() { } + + @Override + public String getType() { + return "prerequisite"; + } } 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 2edfac2b94c1..3f2b1e9dc57e 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 @@ -132,8 +132,8 @@ SELECT COUNT(c) > 0 @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 = { "competencies", "prerequisites", "learningPaths", "learningPaths.competencies" }) + Optional findWithEagerLearningPathsAndCompetenciesAndPrerequisitesById(long courseId); // Note: we load attachments directly because otherwise, they will be loaded in subsequent DB calls due to the EAGER relationship @EntityGraph(type = LOAD, attributePaths = { "lectures", "lectures.attachments" }) @@ -486,8 +486,8 @@ default Course findWithEagerLearningPathsByIdElseThrow(long courseId) { } @NotNull - default Course findWithEagerLearningPathsAndCompetenciesByIdElseThrow(long courseId) { - return findWithEagerLearningPathsAndCompetenciesById(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId)); + default Course findWithEagerLearningPathsAndCompetenciesAndPrerequisitesByIdElseThrow(long courseId) { + return findWithEagerLearningPathsAndCompetenciesAndPrerequisitesById(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId)); } Page findByTitleIgnoreCaseContaining(String partialTitle, Pageable pageable); diff --git a/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathService.java index 1ff89ddaa23d..c69187e8dcdc 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathService.java @@ -176,7 +176,7 @@ public SearchResultPageDTO getAllOfCourseOnPageWithS * @param courseId course id that the learning paths belong to */ public void linkCompetencyToLearningPathsOfCourse(@NotNull Competency competency, long courseId) { - var course = courseRepository.findWithEagerLearningPathsAndCompetenciesByIdElseThrow(courseId); + var course = courseRepository.findWithEagerLearningPathsByIdElseThrow(courseId); var learningPaths = course.getLearningPaths(); learningPaths.forEach(learningPath -> learningPath.addCompetency(competency)); learningPathRepository.saveAll(learningPaths); @@ -193,7 +193,7 @@ public void linkCompetenciesToLearningPathsOfCourse(@NotNull List co if (competencies.isEmpty()) { return; } - var course = courseRepository.findWithEagerLearningPathsAndCompetenciesByIdElseThrow(courseId); + var course = courseRepository.findWithEagerLearningPathsByIdElseThrow(courseId); var learningPaths = course.getLearningPaths(); learningPaths.forEach(learningPath -> learningPath.addCompetencies(new HashSet<>(competencies))); learningPathRepository.saveAll(learningPaths); @@ -207,7 +207,7 @@ public void linkCompetenciesToLearningPathsOfCourse(@NotNull List co * @param courseId course id that the learning paths belong to */ public void removeLinkedCompetencyFromLearningPathsOfCourse(@NotNull Competency competency, long courseId) { - var course = courseRepository.findWithEagerLearningPathsAndCompetenciesByIdElseThrow(courseId); + var course = courseRepository.findWithEagerLearningPathsByIdElseThrow(courseId); var learningPaths = course.getLearningPaths(); learningPaths.forEach(learningPath -> learningPath.removeCompetency(competency)); learningPathRepository.saveAll(learningPaths); diff --git a/src/test/java/de/tum/in/www1/artemis/competency/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/competency/LearningPathIntegrationTest.java index 8738d5fbe018..0a8d7cc084b8 100644 --- a/src/test/java/de/tum/in/www1/artemis/competency/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/competency/LearningPathIntegrationTest.java @@ -251,7 +251,7 @@ void testAll_asEditor() throws Exception { @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") void testEnableLearningPaths() throws Exception { enableLearningPathsRESTCall(course); - final var updatedCourse = courseRepository.findWithEagerLearningPathsAndCompetenciesByIdElseThrow(course.getId()); + final var updatedCourse = courseRepository.findWithEagerLearningPathsAndCompetenciesAndPrerequisitesByIdElseThrow(course.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); @@ -264,7 +264,7 @@ void testEnableLearningPaths() throws Exception { void testEnableLearningPathsWithNoCompetencies() throws Exception { var courseWithoutCompetencies = courseUtilService.createCoursesWithExercisesAndLectures(TEST_PREFIX, false, false, 0).getFirst(); enableLearningPathsRESTCall(courseWithoutCompetencies); - final var updatedCourse = courseRepository.findWithEagerLearningPathsAndCompetenciesByIdElseThrow(courseWithoutCompetencies.getId()); + final var updatedCourse = courseRepository.findWithEagerLearningPathsAndCompetenciesAndPrerequisitesByIdElseThrow(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); 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 d63ea676f711..b719bf326bee 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 @@ -43,7 +43,7 @@ public class LearningPathUtilService { * @return the updated course */ public Course enableAndGenerateLearningPathsForCourse(Course course) { - var eagerlyLoadedCourse = courseRepository.findWithEagerLearningPathsAndCompetenciesByIdElseThrow(course.getId()); + var eagerlyLoadedCourse = courseRepository.findWithEagerLearningPathsAndCompetenciesAndPrerequisitesByIdElseThrow(course.getId()); learningPathService.generateLearningPaths(eagerlyLoadedCourse); eagerlyLoadedCourse.setLearningPathsEnabled(true); return courseRepository.save(eagerlyLoadedCourse); 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 ab413d2d0c65..b9db43c15f18 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 @@ -357,7 +357,11 @@ private void testAllPreAuthorizeInstructor() throws Exception { request.post("/api/courses/" + course.getId() + "/competencies/import-all/1", null, HttpStatus.FORBIDDEN); request.post("/api/courses/" + course.getId() + "/competencies/import", competency, HttpStatus.FORBIDDEN); // relations - request.post("/api/courses/" + course.getId() + "/competencies/relations", new CompetencyRelation(), HttpStatus.FORBIDDEN); + CompetencyRelation relation = new CompetencyRelation(); + relation.setHeadCompetency(competency); + relation.setTailCompetency(competency); + relation.setType(RelationType.EXTENDS); + request.post("/api/courses/" + course.getId() + "/competencies/relations", CompetencyRelationDTO.of(relation), HttpStatus.FORBIDDEN); request.getSet("/api/courses/" + course.getId() + "/competencies/relations", HttpStatus.FORBIDDEN, CompetencyRelationDTO.class); request.delete("/api/courses/" + course.getId() + "/competencies/relations/1", HttpStatus.FORBIDDEN); } @@ -529,7 +533,8 @@ void shouldCreateForInstructor() throws Exception { relationToCreate.setHeadCompetency(headCompetency); relationToCreate.setType(RelationType.EXTENDS); - request.postWithResponseBody("/api/courses/" + course.getId() + "/competencies/relations", relationToCreate, CompetencyRelation.class, HttpStatus.OK); + request.postWithResponseBody("/api/courses/" + course.getId() + "/competencies/relations", CompetencyRelationDTO.of(relationToCreate), CompetencyRelation.class, + HttpStatus.OK); var relations = competencyRelationRepository.findAllWithHeadAndTailByCourseId(course.getId()); assertThat(relations).hasSize(1); @@ -565,7 +570,7 @@ void shouldReturnBadRequestWhenTypeNotPresent() throws Exception { // relation type must be set relationToCreate.setType(null); - request.post("/api/courses/" + course.getId() + "/competencies/relations", relationToCreate, HttpStatus.BAD_REQUEST); + request.post("/api/courses/" + course.getId() + "/competencies/relations", CompetencyRelationDTO.of(relationToCreate), HttpStatus.BAD_REQUEST); } @Test @@ -582,7 +587,7 @@ void shouldReturnBadRequestForCircularRelations() throws Exception { relation.setHeadCompetency(competency); relation.setType(RelationType.ASSUMES); - request.post("/api/courses/" + course.getId() + "/competencies/relations", relation, HttpStatus.BAD_REQUEST); + request.post("/api/courses/" + course.getId() + "/competencies/relations", CompetencyRelationDTO.of(relation), HttpStatus.BAD_REQUEST); } } @@ -1039,7 +1044,7 @@ void shouldImportCompetencies() throws Exception { Competency head = createCompetency(course2); Competency tail = createCompetency(course2); createRelation(tail, head, RelationType.RELATES); - var competencyList = List.of(head, tail); + List competencyList = List.of(head, tail); competencyDTOList = request.postListWithResponseBody("/api/courses/" + course.getId() + "/competencies/import/bulk?importRelations=true", competencyList, CompetencyWithTailRelationDTO.class, HttpStatus.CREATED); From 7e4b138713643fcb16e430f0c7d38c3d57229572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20St=C3=B6hr?= Date: Tue, 2 Jul 2024 18:43:24 +0200 Subject: [PATCH 03/11] Fix more tests and fix prerequisite stuff --- .../repository/CompetencyRepository.java | 15 ----- .../CourseCompetencyRepository.java | 15 +++++ .../artemis/repository/CourseRepository.java | 14 +++-- .../www1/artemis/service/ExerciseService.java | 4 +- .../artemis/service/LectureUnitService.java | 5 +- .../service/competency/CompetencyService.java | 32 +--------- .../competency/CourseCompetencyService.java | 44 ++++++++++++- .../competency/PrerequisiteService.java | 61 +++++++++++++------ .../learningpath/LearningPathService.java | 13 ++-- .../rest/competency/CompetencyResource.java | 32 +++++----- .../rest/competency/PrerequisiteResource.java | 40 +++++++++++- .../CompetencyWithTailRelationDTO.java | 3 +- .../artemis/course/CourseTestService.java | 2 +- .../competency-selection.component.spec.ts | 32 +++++----- 14 files changed, 194 insertions(+), 118 deletions(-) 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 a064690fce2a..43fbf79886fe 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 @@ -59,17 +59,6 @@ public interface CompetencyRepository extends ArtemisJpaRepository findByIdWithExercises(@Param("competencyId") long competencyId); - @Query(""" - SELECT c - FROM Competency c - LEFT JOIN FETCH c.exercises ex - LEFT JOIN FETCH ex.competencies - LEFT JOIN FETCH c.lectureUnits lu - LEFT JOIN FETCH lu.competencies - WHERE c.id = :competencyId - """) - Optional findByIdWithExercisesAndLectureUnitsBidirectional(@Param("competencyId") long competencyId); - /** * Query which fetches all competencies for which the user is editor or instructor in the course and * matching the search criteria. @@ -141,10 +130,6 @@ default Competency findByIdWithExercisesElseThrow(long competencyId) { return getValueElseThrow(findByIdWithExercises(competencyId), competencyId); } - default Competency findByIdWithExercisesAndLectureUnitsBidirectionalElseThrow(long competencyId) { - return getValueElseThrow(findByIdWithExercisesAndLectureUnitsBidirectional(competencyId), competencyId); - } - default Competency findWithLectureUnitsAndExercisesByIdElseThrow(long competencyId) { return getValueElseThrow(findWithLectureUnitsAndExercisesById(competencyId), competencyId); } diff --git a/src/main/java/de/tum/in/www1/artemis/repository/CourseCompetencyRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/CourseCompetencyRepository.java index cfa2e1ac9ce3..0774427eb287 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/CourseCompetencyRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/CourseCompetencyRepository.java @@ -86,6 +86,17 @@ GROUP BY ex.maxPoints, ex.difficulty, TYPE(ex), sS.lastScore, tS.lastScore, sS.l """) Optional findByIdWithExercisesAndLectureUnits(@Param("competencyId") long competencyId); + @Query(""" + SELECT c + FROM CourseCompetency c + LEFT JOIN FETCH c.exercises ex + LEFT JOIN FETCH ex.competencies + LEFT JOIN FETCH c.lectureUnits lu + LEFT JOIN FETCH lu.competencies + WHERE c.id = :competencyId + """) + Optional findByIdWithExercisesAndLectureUnitsBidirectional(@Param("competencyId") long competencyId); + /** * Finds a list of competencies by id and verifies that the user is at least editor in the respective courses. * If any of the competencies are not accessible, throws a {@link EntityNotFoundException} @@ -124,5 +135,9 @@ default CourseCompetency findByIdWithExercisesAndLectureUnitsElseThrow(long comp return getValueElseThrow(findByIdWithExercisesAndLectureUnits(competencyId), competencyId); } + default CourseCompetency findByIdWithExercisesAndLectureUnitsBidirectionalElseThrow(long competencyId) { + return getValueElseThrow(findByIdWithExercisesAndLectureUnitsBidirectional(competencyId), competencyId); + } + List findByCourseIdOrderById(long courseId); } 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 3f2b1e9dc57e..7b0bc8913aa6 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 @@ -129,8 +129,14 @@ SELECT COUNT(c) > 0 @EntityGraph(type = LOAD, attributePaths = { "competencies", "prerequisites" }) Optional findWithEagerCompetenciesAndPrerequisitesById(long courseId); - @EntityGraph(type = LOAD, attributePaths = { "learningPaths" }) - Optional findWithEagerLearningPathsById(long courseId); + @Query(""" + SELECT c + FROM Course c + LEFT JOIN FETCH c.learningPaths learningPaths + LEFT JOIN FETCH learningPaths.competencies + WHERE c.id = :courseId + """) + Optional findWithEagerLearningPathsAndLearningPathCompetencies(@Param("courseId") long courseId); @EntityGraph(type = LOAD, attributePaths = { "competencies", "prerequisites", "learningPaths", "learningPaths.competencies" }) Optional findWithEagerLearningPathsAndCompetenciesAndPrerequisitesById(long courseId); @@ -481,8 +487,8 @@ default Course findWithEagerCompetenciesAndPrerequisitesByIdElseThrow(long cours } @NotNull - default Course findWithEagerLearningPathsByIdElseThrow(long courseId) { - return findWithEagerLearningPathsById(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId)); + default Course findWithEagerLearningPathsAndLearningPathCompetenciesByIdElseThrow(long courseId) { + return getValueElseThrow(findWithEagerLearningPathsAndLearningPathCompetencies(courseId), courseId); } @NotNull diff --git a/src/main/java/de/tum/in/www1/artemis/service/ExerciseService.java b/src/main/java/de/tum/in/www1/artemis/service/ExerciseService.java index cfb582cba8b6..ec085efd1992 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ExerciseService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ExerciseService.java @@ -38,7 +38,7 @@ import de.tum.in.www1.artemis.domain.Submission; import de.tum.in.www1.artemis.domain.Team; 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.CourseCompetency; import de.tum.in.www1.artemis.domain.enumeration.ComplaintType; import de.tum.in.www1.artemis.domain.enumeration.ExerciseMode; import de.tum.in.www1.artemis.domain.lti.LtiResourceLaunch; @@ -757,7 +757,7 @@ public List getFeedbackToBeDeletedAfterGradingInstructionUpdate(boolea * @param exercises set of exercises * @param competency competency to remove */ - public void removeCompetency(@NotNull Set exercises, @NotNull Competency competency) { + public void removeCompetency(@NotNull Set exercises, @NotNull CourseCompetency competency) { exercises.forEach(exercise -> exercise.getCompetencies().remove(competency)); exerciseRepository.saveAll(exercises); } 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 725533ccdebb..21ca5ab0c186 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 @@ -24,7 +24,6 @@ import de.tum.in.www1.artemis.domain.Exercise; 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.competency.CourseCompetency; import de.tum.in.www1.artemis.domain.lecture.AttachmentUnit; import de.tum.in.www1.artemis.domain.lecture.ExerciseUnit; @@ -186,7 +185,7 @@ public void removeLectureUnit(@NotNull LectureUnit lectureUnit) { * @param lectureUnitsToAdd A set of lecture units to link to the specified competency * @param lectureUnitsToRemove A set of lecture units to unlink from the specified competency */ - public void linkLectureUnitsToCompetency(Competency competency, Set lectureUnitsToAdd, Set lectureUnitsToRemove) { + public void linkLectureUnitsToCompetency(CourseCompetency competency, Set lectureUnitsToAdd, Set lectureUnitsToRemove) { final Predicate isExerciseUnit = lectureUnit -> lectureUnit instanceof ExerciseUnit; // Remove the competency from the old lecture units @@ -213,7 +212,7 @@ public void linkLectureUnitsToCompetency(Competency competency, Set * @param lectureUnits set of lecture units * @param competency competency to remove */ - public void removeCompetency(Set lectureUnits, Competency competency) { + public void removeCompetency(Set lectureUnits, CourseCompetency competency) { lectureUnits.forEach(lectureUnit -> lectureUnit.getCompetencies().remove(competency)); lectureUnitRepository.saveAll(lectureUnits); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/competency/CompetencyService.java b/src/main/java/de/tum/in/www1/artemis/service/competency/CompetencyService.java index 732eab4d22cd..bf434a009e66 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/competency/CompetencyService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/competency/CompetencyService.java @@ -27,12 +27,10 @@ import de.tum.in.www1.artemis.repository.CompetencyProgressRepository; import de.tum.in.www1.artemis.repository.CompetencyRelationRepository; import de.tum.in.www1.artemis.repository.CompetencyRepository; -import de.tum.in.www1.artemis.repository.CourseCompetencyRepository; import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.LectureUnitCompletionRepository; import de.tum.in.www1.artemis.repository.competency.StandardizedCompetencyRepository; import de.tum.in.www1.artemis.service.AuthorizationCheckService; -import de.tum.in.www1.artemis.service.ExerciseService; import de.tum.in.www1.artemis.service.LectureUnitService; import de.tum.in.www1.artemis.service.learningpath.LearningPathService; import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; @@ -66,8 +64,6 @@ public class CompetencyService { private final LectureUnitService lectureUnitService; - private final ExerciseService exerciseService; - private final CompetencyProgressRepository competencyProgressRepository; private final LectureUnitCompletionRepository lectureUnitCompletionRepository; @@ -76,24 +72,20 @@ public class CompetencyService { private final CourseRepository courseRepository; - private final CourseCompetencyRepository courseCompetencyRepository; - public CompetencyService(CompetencyRepository competencyRepository, AuthorizationCheckService authCheckService, CompetencyRelationRepository competencyRelationRepository, - LearningPathService learningPathService, CompetencyProgressService competencyProgressService, LectureUnitService lectureUnitService, ExerciseService exerciseService, + LearningPathService learningPathService, CompetencyProgressService competencyProgressService, LectureUnitService lectureUnitService, CompetencyProgressRepository competencyProgressRepository, LectureUnitCompletionRepository lectureUnitCompletionRepository, - StandardizedCompetencyRepository standardizedCompetencyRepository, CourseRepository courseRepository, CourseCompetencyRepository courseCompetencyRepository) { + StandardizedCompetencyRepository standardizedCompetencyRepository, CourseRepository courseRepository) { this.competencyRepository = competencyRepository; this.authCheckService = authCheckService; this.competencyRelationRepository = competencyRelationRepository; this.learningPathService = learningPathService; this.competencyProgressService = competencyProgressService; this.lectureUnitService = lectureUnitService; - this.exerciseService = exerciseService; this.competencyProgressRepository = competencyProgressRepository; this.lectureUnitCompletionRepository = lectureUnitCompletionRepository; this.standardizedCompetencyRepository = standardizedCompetencyRepository; this.courseRepository = courseRepository; - this.courseCompetencyRepository = courseCompetencyRepository; } /** @@ -296,26 +288,6 @@ public Competency updateCompetency(Competency competencyToUpdate, Competency com return persistedCompetency; } - /** - * Deletes a competency and all its relations. - * - * @param competency the competency to delete - * @param course the course the competency belongs to - */ - public void deleteCompetency(Competency competency, Course course) { - competencyRelationRepository.deleteAllByCompetencyId(competency.getId()); - competencyProgressService.deleteProgressForCompetency(competency.getId()); - - exerciseService.removeCompetency(competency.getExercises(), competency); - lectureUnitService.removeCompetency(competency.getLectureUnits(), competency); - - if (course.getLearningPathsEnabled()) { - learningPathService.removeLinkedCompetencyFromLearningPathsOfCourse(competency, course.getId()); - } - - competencyRepository.deleteById(competency.getId()); - } - /** * Finds a competency by its id and fetches its lecture units, exercises and progress for the provided user. It also fetches the lecture unit progress for the same user. *

diff --git a/src/main/java/de/tum/in/www1/artemis/service/competency/CourseCompetencyService.java b/src/main/java/de/tum/in/www1/artemis/service/competency/CourseCompetencyService.java index e2a0ba12b833..628a1da547f0 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/competency/CourseCompetencyService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/competency/CourseCompetencyService.java @@ -10,9 +10,14 @@ import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; +import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.competency.CourseCompetency; import de.tum.in.www1.artemis.repository.CompetencyProgressRepository; +import de.tum.in.www1.artemis.repository.CompetencyRelationRepository; import de.tum.in.www1.artemis.repository.CourseCompetencyRepository; +import de.tum.in.www1.artemis.service.ExerciseService; +import de.tum.in.www1.artemis.service.LectureUnitService; +import de.tum.in.www1.artemis.service.learningpath.LearningPathService; /** * Service for managing competencies. @@ -25,9 +30,26 @@ public class CourseCompetencyService { private final CourseCompetencyRepository courseCompetencyRepository; - public CourseCompetencyService(CompetencyProgressRepository competencyProgressRepository, CourseCompetencyRepository courseCompetencyRepository) { + private final CompetencyRelationRepository competencyRelationRepository; + + private final CompetencyProgressService competencyProgressService; + + private final ExerciseService exerciseService; + + private final LectureUnitService lectureUnitService; + + private final LearningPathService learningPathService; + + public CourseCompetencyService(CompetencyProgressRepository competencyProgressRepository, CourseCompetencyRepository courseCompetencyRepository, + CompetencyRelationRepository competencyRelationRepository, CompetencyProgressService competencyProgressService, ExerciseService exerciseService, + LectureUnitService lectureUnitService, LearningPathService learningPathService) { this.competencyProgressRepository = competencyProgressRepository; this.courseCompetencyRepository = courseCompetencyRepository; + this.competencyRelationRepository = competencyRelationRepository; + this.competencyProgressService = competencyProgressService; + this.exerciseService = exerciseService; + this.lectureUnitService = lectureUnitService; + this.learningPathService = learningPathService; } /** @@ -55,4 +77,24 @@ public List findCourseCompetenciesWithProgressForUserByCourseI return competencies; } + + /** + * Deletes a course competency and all its relations. + * + * @param courseCompetency the course competency to delete + * @param course the course the competency belongs to + */ + public void deleteCourseCompetency(CourseCompetency courseCompetency, Course course) { + competencyRelationRepository.deleteAllByCompetencyId(courseCompetency.getId()); + competencyProgressService.deleteProgressForCompetency(courseCompetency.getId()); + + exerciseService.removeCompetency(courseCompetency.getExercises(), courseCompetency); + lectureUnitService.removeCompetency(courseCompetency.getLectureUnits(), courseCompetency); + + if (course.getLearningPathsEnabled()) { + learningPathService.removeLinkedCompetencyFromLearningPathsOfCourse(courseCompetency, course.getId()); + } + + courseCompetencyRepository.deleteById(courseCompetency.getId()); + } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/competency/PrerequisiteService.java b/src/main/java/de/tum/in/www1/artemis/service/competency/PrerequisiteService.java index 75be985f8e51..267d3c4b8c72 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/competency/PrerequisiteService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/competency/PrerequisiteService.java @@ -2,8 +2,9 @@ import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; -import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import jakarta.ws.rs.BadRequestException; @@ -11,13 +12,16 @@ import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; +import de.tum.in.www1.artemis.domain.competency.CompetencyRelation; import de.tum.in.www1.artemis.domain.competency.CourseCompetency; import de.tum.in.www1.artemis.domain.competency.Prerequisite; +import de.tum.in.www1.artemis.repository.CompetencyRelationRepository; import de.tum.in.www1.artemis.repository.CourseCompetencyRepository; import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.PrerequisiteRepository; import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.service.AuthorizationCheckService; +import de.tum.in.www1.artemis.service.learningpath.LearningPathService; import de.tum.in.www1.artemis.web.rest.dto.competency.PrerequisiteRequestDTO; /** @@ -37,13 +41,20 @@ public class PrerequisiteService { private final AuthorizationCheckService authorizationCheckService; + private final LearningPathService learningPathService; + + private final CompetencyRelationRepository competencyRelationRepository; + public PrerequisiteService(PrerequisiteRepository prerequisiteRepository, CourseRepository courseRepository, CourseCompetencyRepository courseCompetencyRepository, - UserRepository userRepository, AuthorizationCheckService authorizationCheckService) { + UserRepository userRepository, AuthorizationCheckService authorizationCheckService, LearningPathService learningPathService, + CompetencyRelationRepository competencyRelationRepository) { this.prerequisiteRepository = prerequisiteRepository; this.courseRepository = courseRepository; this.courseCompetencyRepository = courseCompetencyRepository; this.userRepository = userRepository; this.authorizationCheckService = authorizationCheckService; + this.learningPathService = learningPathService; + this.competencyRelationRepository = competencyRelationRepository; } /** @@ -60,7 +71,13 @@ public Prerequisite createPrerequisite(PrerequisiteRequestDTO prerequisiteValues prerequisiteValues.masteryThreshold(), prerequisiteValues.taxonomy(), prerequisiteValues.optional()); prerequisiteToCreate.setCourse(course); - return prerequisiteRepository.save(prerequisiteToCreate); + var persistedPrerequisite = prerequisiteRepository.save(prerequisiteToCreate); + + if (course.getLearningPathsEnabled()) { + learningPathService.linkCompetencyToLearningPathsOfCourse(persistedPrerequisite, course.getId()); + } + + return persistedPrerequisite; } /** @@ -84,17 +101,6 @@ public Prerequisite updatePrerequisite(PrerequisiteRequestDTO prerequisiteValues return prerequisiteRepository.save(existingPrerequisite); } - /** - * Deletes an existing prerequisite if it is part of the given course or throws an EntityNotFoundException - * - * @param prerequisiteId the id of the prerequisite to delete - * @param courseId the id of the course the prerequisite is part of - */ - public void deletePrerequisite(long prerequisiteId, long courseId) { - prerequisiteRepository.findByIdAndCourseIdElseThrow(prerequisiteId, courseId); - prerequisiteRepository.deleteById(prerequisiteId); - } - /** * Imports the courseCompetencies with the given ids as prerequisites into a course * @@ -102,10 +108,11 @@ public void deletePrerequisite(long prerequisiteId, long courseId) { * @param courseCompetencyIds the ids of the courseCompetencies to import * @return The list of imported prerequisites */ - public List importPrerequisites(long courseId, List courseCompetencyIds) { + public List importPrerequisites(long courseId, List courseCompetencyIds, Set relations) { var course = courseRepository.findByIdElseThrow(courseId); var user = userRepository.getUserWithGroupsAndAuthorities(); List courseCompetenciesToImport; + var idToImportedCompetency = new HashMap(); if (authorizationCheckService.isAdmin(user)) { courseCompetenciesToImport = courseCompetencyRepository.findAllByIdElseThrow(courseCompetencyIds); } @@ -119,7 +126,6 @@ public List importPrerequisites(long courseId, List courseCo throw new BadRequestException("You may not import a competency as prerequisite into the same course!"); } - var prerequisitesToImport = new ArrayList(); for (var competency : courseCompetenciesToImport) { var prerequisiteToImport = new Prerequisite(); prerequisiteToImport.setTitle(competency.getTitle()); @@ -131,12 +137,27 @@ public List importPrerequisites(long courseId, List courseCo prerequisiteToImport.setLinkedCourseCompetency(competency); prerequisiteToImport.setCourse(course); - prerequisitesToImport.add(prerequisiteToImport); + idToImportedCompetency.put(competency.getId(), prerequisiteRepository.save(prerequisiteToImport)); + } + + if (course.getLearningPathsEnabled()) { + learningPathService.linkCompetenciesToLearningPathsOfCourse(idToImportedCompetency.values().stream().toList(), course.getId()); } - // TODO: link to learning paths once we support them for prerequisites. - // TODO: import relations once we support them for prerequisites. + if (!relations.isEmpty()) { + for (var relation : relations) { + var tailCompetency = idToImportedCompetency.get(relation.getTailCompetency().getId()); + var headCompetency = idToImportedCompetency.get(relation.getHeadCompetency().getId()); + + CompetencyRelation relationToImport = new CompetencyRelation(); + relationToImport.setType(relation.getType()); + relationToImport.setTailCompetency(tailCompetency); + relationToImport.setHeadCompetency(headCompetency); + + competencyRelationRepository.save(relationToImport); + } + } - return prerequisiteRepository.saveAll(prerequisitesToImport); + return idToImportedCompetency.values().stream().toList(); } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathService.java b/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathService.java index c69187e8dcdc..3d068ce9be93 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathService.java @@ -22,7 +22,6 @@ import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.Exercise; 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.CompetencyProgress; import de.tum.in.www1.artemis.domain.competency.CompetencyRelation; import de.tum.in.www1.artemis.domain.competency.CourseCompetency; @@ -175,8 +174,8 @@ public SearchResultPageDTO getAllOfCourseOnPageWithS * @param competency Competency that should be added to each learning path * @param courseId course id that the learning paths belong to */ - public void linkCompetencyToLearningPathsOfCourse(@NotNull Competency competency, long courseId) { - var course = courseRepository.findWithEagerLearningPathsByIdElseThrow(courseId); + public void linkCompetencyToLearningPathsOfCourse(@NotNull CourseCompetency competency, long courseId) { + var course = courseRepository.findWithEagerLearningPathsAndLearningPathCompetenciesByIdElseThrow(courseId); var learningPaths = course.getLearningPaths(); learningPaths.forEach(learningPath -> learningPath.addCompetency(competency)); learningPathRepository.saveAll(learningPaths); @@ -189,11 +188,11 @@ public void linkCompetencyToLearningPathsOfCourse(@NotNull Competency competency * @param competencies The list of competencies that should be added * @param courseId course id that the learning paths belong to */ - public void linkCompetenciesToLearningPathsOfCourse(@NotNull List competencies, long courseId) { + public void linkCompetenciesToLearningPathsOfCourse(@NotNull List competencies, long courseId) { if (competencies.isEmpty()) { return; } - var course = courseRepository.findWithEagerLearningPathsByIdElseThrow(courseId); + var course = courseRepository.findWithEagerLearningPathsAndLearningPathCompetenciesByIdElseThrow(courseId); var learningPaths = course.getLearningPaths(); learningPaths.forEach(learningPath -> learningPath.addCompetencies(new HashSet<>(competencies))); learningPathRepository.saveAll(learningPaths); @@ -206,8 +205,8 @@ public void linkCompetenciesToLearningPathsOfCourse(@NotNull List 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(@NotNull Competency competency, long courseId) { - var course = courseRepository.findWithEagerLearningPathsByIdElseThrow(courseId); + public void removeLinkedCompetencyFromLearningPathsOfCourse(@NotNull CourseCompetency competency, long courseId) { + var course = courseRepository.findWithEagerLearningPathsAndLearningPathCompetenciesByIdElseThrow(courseId); var learningPaths = course.getLearningPaths(); learningPaths.forEach(learningPath -> learningPath.removeCompetency(competency)); learningPathRepository.saveAll(learningPaths); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/competency/CompetencyResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/competency/CompetencyResource.java index 96d294e74a41..76fcc1636d65 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/competency/CompetencyResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/competency/CompetencyResource.java @@ -56,6 +56,7 @@ import de.tum.in.www1.artemis.service.competency.CompetencyProgressService; import de.tum.in.www1.artemis.service.competency.CompetencyRelationService; import de.tum.in.www1.artemis.service.competency.CompetencyService; +import de.tum.in.www1.artemis.service.competency.CourseCompetencyService; import de.tum.in.www1.artemis.service.feature.Feature; import de.tum.in.www1.artemis.service.feature.FeatureToggle; import de.tum.in.www1.artemis.service.iris.session.IrisCompetencyGenerationSessionService; @@ -110,12 +111,14 @@ public class CompetencyResource { private final CompetencyJolService competencyJolService; + private final CourseCompetencyService courseCompetencyService; + public CompetencyResource(CourseRepository courseRepository, AuthorizationCheckService authorizationCheckService, UserRepository userRepository, CompetencyRepository competencyRepository, CompetencyRelationRepository competencyRelationRepository, CompetencyService competencyService, CompetencyProgressRepository competencyProgressRepository, CompetencyProgressService competencyProgressService, ExerciseService exerciseService, LectureUnitService lectureUnitService, CompetencyRelationService competencyRelationService, Optional irisCompetencyGenerationSessionService, CourseCompetencyRepository courseCompetencyRepository, - CompetencyJolService competencyJolService) { + CompetencyJolService competencyJolService, CourseCompetencyService courseCompetencyService) { this.courseRepository = courseRepository; this.competencyRelationRepository = competencyRelationRepository; this.authorizationCheckService = authorizationCheckService; @@ -130,6 +133,7 @@ public CompetencyResource(CourseRepository courseRepository, AuthorizationCheckS this.irisCompetencyGenerationSessionService = irisCompetencyGenerationSessionService; this.courseCompetencyRepository = courseCompetencyRepository; this.competencyJolService = competencyJolService; + this.courseCompetencyService = courseCompetencyService; } /** @@ -189,7 +193,7 @@ public ResponseEntity getCompetency(@PathVariable long competencyId, var currentUser = userRepository.getUserWithGroupsAndAuthorities(); var course = courseRepository.findByIdElseThrow(courseId); var competency = competencyService.findCompetencyWithExercisesAndLectureUnitsAndProgressForUser(competencyId, currentUser.getId()); - checkAuthorizationForCompetency(Role.STUDENT, course, competency); + checkAuthorizationForCompetency(course, competency); competency.setLectureUnits(competency.getLectureUnits().stream().filter(lectureUnit -> authorizationCheckService.isAllowedToSeeLectureUnit(lectureUnit, currentUser)) .peek(lectureUnit -> lectureUnit.setCompleted(lectureUnit.isCompletedFor(currentUser))).collect(Collectors.toSet())); @@ -219,7 +223,7 @@ public ResponseEntity updateCompetency(@PathVariable long courseId, } var course = courseRepository.findByIdElseThrow(courseId); var existingCompetency = competencyRepository.findByIdWithLectureUnitsElseThrow(competency.getId()); - checkAuthorizationForCompetency(Role.INSTRUCTOR, course, existingCompetency); + checkAuthorizationForCompetency(course, existingCompetency); var persistedCompetency = competencyService.updateCompetency(existingCompetency, competency); lectureUnitService.linkLectureUnitsToCompetency(persistedCompetency, competency.getLectureUnits(), existingCompetency.getLectureUnits()); @@ -398,10 +402,10 @@ public ResponseEntity deleteCompetency(@PathVariable long competencyId, @P log.info("REST request to delete a Competency : {}", competencyId); var course = courseRepository.findByIdElseThrow(courseId); - var competency = competencyRepository.findByIdWithExercisesAndLectureUnitsBidirectionalElseThrow(competencyId); - checkAuthorizationForCompetency(Role.INSTRUCTOR, course, competency); + var competency = courseCompetencyRepository.findByIdWithExercisesAndLectureUnitsBidirectionalElseThrow(competencyId); + checkAuthorizationForCompetency(course, competency); - competencyService.deleteCompetency(competency, course); + courseCompetencyService.deleteCourseCompetency(competency, course); return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, competency.getTitle())).build(); } @@ -422,7 +426,7 @@ public ResponseEntity getCompetencyStudentProgress(@PathVari var user = userRepository.getUserWithGroupsAndAuthorities(); var course = courseRepository.findByIdElseThrow(courseId); var competency = competencyRepository.findByIdElseThrow(competencyId); - checkAuthorizationForCompetency(Role.STUDENT, course, competency); + checkAuthorizationForCompetency(course, competency); CompetencyProgress studentProgress; if (refresh) { @@ -489,9 +493,9 @@ public ResponseEntity createCompetencyRelation(@PathVaria var course = courseRepository.findByIdElseThrow(courseId); var tailCompetency = courseCompetencyRepository.findByIdElseThrow(tailId); - checkAuthorizationForCompetency(Role.INSTRUCTOR, course, tailCompetency); + checkAuthorizationForCompetency(course, tailCompetency); var headCompetency = courseCompetencyRepository.findByIdElseThrow(headId); - checkAuthorizationForCompetency(Role.INSTRUCTOR, course, headCompetency); + checkAuthorizationForCompetency(course, headCompetency); var createdRelation = competencyRelationService.createCompetencyRelation(tailCompetency, headCompetency, relation.relationType(), course); @@ -512,8 +516,8 @@ public ResponseEntity removeCompetencyRelation(@PathVariable long courseId var course = courseRepository.findByIdElseThrow(courseId); var relation = competencyRelationRepository.findById(competencyRelationId).orElseThrow(); - checkAuthorizationForCompetency(Role.INSTRUCTOR, course, relation.getTailCompetency()); - checkAuthorizationForCompetency(Role.INSTRUCTOR, course, relation.getHeadCompetency()); + checkAuthorizationForCompetency(course, relation.getTailCompetency()); + checkAuthorizationForCompetency(course, relation.getHeadCompetency()); competencyRelationRepository.delete(relation); @@ -555,20 +559,18 @@ public ResponseEntity> getCourseCompetencyTitles(@PathVariable Long } /** - * Checks if the user has the necessary permissions and the competency matches the course. + * Checks if the competency matches the course. * - * @param role The minimal role the user must have in the course * @param course The course for which to check the authorization role for * @param competency The competency to be accessed by the user */ - private void checkAuthorizationForCompetency(Role role, @NotNull Course course, @NotNull CourseCompetency competency) { + private void checkAuthorizationForCompetency(@NotNull Course course, @NotNull CourseCompetency competency) { if (competency.getCourse() == null) { throw new BadRequestAlertException("A competency must belong to a course", ENTITY_NAME, "competencyNoCourse"); } if (!competency.getCourse().getId().equals(course.getId())) { throw new BadRequestAlertException("The competency does not belong to the correct course", ENTITY_NAME, "competencyWrongCourse"); } - authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(role, course, null); } /** diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/competency/PrerequisiteResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/competency/PrerequisiteResource.java index 3843a34b6a24..e9d34c823fa9 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/competency/PrerequisiteResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/competency/PrerequisiteResource.java @@ -5,6 +5,9 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.List; +import java.util.Set; + +import jakarta.validation.constraints.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -20,6 +23,8 @@ import org.springframework.web.bind.annotation.RestController; import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.competency.CourseCompetency; +import de.tum.in.www1.artemis.repository.CourseCompetencyRepository; import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.PrerequisiteRepository; import de.tum.in.www1.artemis.security.Role; @@ -27,9 +32,11 @@ import de.tum.in.www1.artemis.security.annotations.enforceRoleInCourse.EnforceAtLeastEditorInCourse; import de.tum.in.www1.artemis.security.annotations.enforceRoleInCourse.EnforceAtLeastStudentInCourse; import de.tum.in.www1.artemis.service.AuthorizationCheckService; +import de.tum.in.www1.artemis.service.competency.CourseCompetencyService; import de.tum.in.www1.artemis.service.competency.PrerequisiteService; import de.tum.in.www1.artemis.web.rest.dto.competency.PrerequisiteRequestDTO; import de.tum.in.www1.artemis.web.rest.dto.competency.PrerequisiteResponseDTO; +import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; /** * REST controller for managing {@link de.tum.in.www1.artemis.domain.competency.Prerequisite Prerequisite} entities. @@ -41,6 +48,8 @@ public class PrerequisiteResource { private static final Logger log = LoggerFactory.getLogger(PrerequisiteResource.class); + private static final String ENTITY_NAME = "prerequisite"; + private final PrerequisiteService prerequisiteService; private final PrerequisiteRepository prerequisiteRepository; @@ -49,12 +58,18 @@ public class PrerequisiteResource { private final AuthorizationCheckService authorizationCheckService; + private final CourseCompetencyService courseCompetencyService; + + private final CourseCompetencyRepository courseCompetencyRepository; + public PrerequisiteResource(PrerequisiteService prerequisiteService, PrerequisiteRepository prerequisiteRepository, CourseRepository courseRepository, - AuthorizationCheckService authorizationCheckService) { + AuthorizationCheckService authorizationCheckService, CourseCompetencyService courseCompetencyService, CourseCompetencyRepository courseCompetencyRepository) { this.prerequisiteService = prerequisiteService; this.prerequisiteRepository = prerequisiteRepository; this.courseRepository = courseRepository; this.authorizationCheckService = authorizationCheckService; + this.courseCompetencyService = courseCompetencyService; + this.courseCompetencyRepository = courseCompetencyRepository; } /** @@ -147,7 +162,10 @@ public ResponseEntity updatePrerequisite(@PathVariable public ResponseEntity deletePrerequisite(@PathVariable long prerequisiteId, @PathVariable long courseId) { log.info("REST request to delete Prerequisite with id : {}", prerequisiteId); - prerequisiteService.deletePrerequisite(prerequisiteId, courseId); + var course = courseRepository.findByIdElseThrow(courseId); + var prerequisite = courseCompetencyRepository.findByIdWithExercisesAndLectureUnitsBidirectionalElseThrow(prerequisiteId); + checkAuthorizationForPrerequisite(course, prerequisite); + courseCompetencyService.deleteCourseCompetency(prerequisite, course); return ResponseEntity.ok().build(); } @@ -165,9 +183,25 @@ public ResponseEntity deletePrerequisite(@PathVariable long prerequisiteId public ResponseEntity> importPrerequisites(@PathVariable long courseId, @RequestBody List courseCompetencyIds) throws URISyntaxException { log.info("REST request to import courseCompetencies with ids {} as prerequisites", courseCompetencyIds); - var importedPrerequisites = prerequisiteService.importPrerequisites(courseId, courseCompetencyIds); + // TODO: Allow import of relations between prerequisites + var importedPrerequisites = prerequisiteService.importPrerequisites(courseId, courseCompetencyIds, Set.of()); return ResponseEntity.created(new URI("/api/courses/" + courseId + "/competencies/prerequisites")) .body(importedPrerequisites.stream().map(PrerequisiteResponseDTO::of).toList()); } + + /** + * Checks if the competency matches the course. + * + * @param course The course for which to check the authorization role for + * @param prerequisite The prerequisite to be accessed by the user + */ + private void checkAuthorizationForPrerequisite(@NotNull Course course, @NotNull CourseCompetency prerequisite) { + if (prerequisite.getCourse() == null) { + throw new BadRequestAlertException("A competency must belong to a course", ENTITY_NAME, "competencyNoCourse"); + } + if (!prerequisite.getCourse().getId().equals(course.getId())) { + throw new BadRequestAlertException("The competency does not belong to the correct course", ENTITY_NAME, "competencyWrongCourse"); + } + } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/CompetencyWithTailRelationDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/CompetencyWithTailRelationDTO.java index 360eff8add79..58f28542ca87 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/CompetencyWithTailRelationDTO.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/CompetencyWithTailRelationDTO.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import de.tum.in.www1.artemis.domain.competency.Competency; +import de.tum.in.www1.artemis.domain.competency.CourseCompetency; /** * DTO containing a {@link Competency} and list of {@link CompetencyRelationDTO CompetencyRelation(DTO)s} for which it is the tail competency. @@ -13,6 +14,6 @@ * @param tailRelations relations where it is the tail competency */ @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record CompetencyWithTailRelationDTO(Competency competency, List tailRelations) { +public record CompetencyWithTailRelationDTO(CourseCompetency competency, List tailRelations) { } 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 551e50550046..c9a91de64d4c 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 @@ -1808,7 +1808,7 @@ public void testAddStudentOrTutorOrEditorOrInstructorToCourse() throws Exception course.setLearningPathsEnabled(true); course = courseRepo.save(course); testAddStudentOrTutorOrEditorOrInstructorToCourse(course, HttpStatus.OK); - course = courseRepo.findWithEagerLearningPathsByIdElseThrow(course.getId()); + course = courseRepo.findWithEagerLearningPathsAndLearningPathCompetenciesByIdElseThrow(course.getId()); assertThat(course.getLearningPaths()).isNotEmpty(); // TODO check that the roles have changed accordingly } diff --git a/src/test/javascript/spec/component/shared/competency-selection.component.spec.ts b/src/test/javascript/spec/component/shared/competency-selection.component.spec.ts index 5a683261a723..92e2d53dfcfb 100644 --- a/src/test/javascript/spec/component/shared/competency-selection.component.spec.ts +++ b/src/test/javascript/spec/component/shared/competency-selection.component.spec.ts @@ -4,7 +4,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { MockComponent, MockDirective, MockModule } from 'ng-mocks'; import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; -import { CompetencyService } from 'app/course/competencies/competency.service'; import { ActivatedRoute, Router, convertToParamMap } from '@angular/router'; import { MockRouter } from '../../helpers/mocks/mock-router'; import { NgModel, ReactiveFormsModule } from '@angular/forms'; @@ -14,12 +13,13 @@ import { HttpResponse } from '@angular/common/http'; import { By } from '@angular/platform-browser'; import { CourseStorageService } from 'app/course/manage/course-storage.service'; import { ChangeDetectorRef } from '@angular/core'; +import { CourseCompetencyService } from 'app/course/competencies/course-competency.service'; describe('CompetencySelection', () => { let fixture: ComponentFixture; let component: CompetencySelectionComponent; let courseStorageService: CourseStorageService; - let competencyService: CompetencyService; + let courseCompetencyService: CourseCompetencyService; beforeEach(() => { TestBed.configureTestingModule({ @@ -45,7 +45,7 @@ describe('CompetencySelection', () => { fixture = TestBed.createComponent(CompetencySelectionComponent); component = fixture.componentInstance; courseStorageService = TestBed.inject(CourseStorageService); - competencyService = TestBed.inject(CompetencyService); + courseCompetencyService = TestBed.inject(CourseCompetencyService); }); }); @@ -57,12 +57,12 @@ describe('CompetencySelection', () => { const nonOptional = { id: 1, optional: false } as Competency; const optional = { id: 2, optional: true } as Competency; const getCourseSpy = jest.spyOn(courseStorageService, 'getCourse').mockReturnValue({ competencies: [nonOptional, optional] }); - const getAllForCourseSpy = jest.spyOn(competencyService, 'getAllForCourse'); + const getAllForCourseSpy = jest.spyOn(courseCompetencyService, 'getAllForCourse'); fixture.detectChanges(); const selector = fixture.debugElement.nativeElement.querySelector('#competency-selector'); - expect(component.value).toBeUndefined(); + expect(component.selectedCompetencies).toBeUndefined(); expect(getCourseSpy).toHaveBeenCalledOnce(); expect(getAllForCourseSpy).not.toHaveBeenCalled(); expect(component.isLoading).toBeFalse(); @@ -74,7 +74,7 @@ describe('CompetencySelection', () => { const nonOptional = { id: 1, optional: false } as Competency; const optional = { id: 2, optional: true } as Competency; const getCourseSpy = jest.spyOn(courseStorageService, 'getCourse').mockReturnValue({ competencies: undefined }); - const getAllForCourseSpy = jest.spyOn(competencyService, 'getAllForCourse').mockReturnValue(of(new HttpResponse({ body: [nonOptional, optional] }))); + const getAllForCourseSpy = jest.spyOn(courseCompetencyService, 'getAllForCourse').mockReturnValue(of(new HttpResponse({ body: [nonOptional, optional] }))); fixture.detectChanges(); @@ -88,7 +88,7 @@ describe('CompetencySelection', () => { it('should set disabled when error during loading', () => { const getCourseSpy = jest.spyOn(courseStorageService, 'getCourse').mockReturnValue({ competencies: undefined }); - const getAllForCourseSpy = jest.spyOn(competencyService, 'getAllForCourse').mockReturnValue(throwError({ status: 500 })); + const getAllForCourseSpy = jest.spyOn(courseCompetencyService, 'getAllForCourse').mockReturnValue(throwError({ status: 500 })); fixture.detectChanges(); @@ -100,7 +100,7 @@ describe('CompetencySelection', () => { it('should be hidden when no competencies', () => { const getCourseSpy = jest.spyOn(courseStorageService, 'getCourse').mockReturnValue({ competencies: [] }); - const getAllForCourseSpy = jest.spyOn(competencyService, 'getAllForCourse').mockReturnValue(of(new HttpResponse({ body: [] }))); + const getAllForCourseSpy = jest.spyOn(courseCompetencyService, 'getAllForCourse').mockReturnValue(of(new HttpResponse({ body: [] }))); fixture.detectChanges(); @@ -118,8 +118,8 @@ describe('CompetencySelection', () => { fixture.detectChanges(); component.writeValue([{ id: 1, title: 'other' } as Competency]); - expect(component.value).toBeArrayOfSize(1); - expect(component.value?.first()?.title).toBe('test'); + expect(component.selectedCompetencies).toBeArrayOfSize(1); + expect(component.selectedCompetencies?.first()?.title).toBe('test'); }); it('should trigger change detection after loading competencies', () => { @@ -139,24 +139,24 @@ describe('CompetencySelection', () => { jest.spyOn(courseStorageService, 'getCourse').mockReturnValue({ competencies: [competency1, competency2, competency3] }); fixture.detectChanges(); - expect(component.value).toBeUndefined(); + expect(component.selectedCompetencies).toBeUndefined(); component.toggleCompetency(competency1); component.toggleCompetency(competency2); component.toggleCompetency(competency3); - expect(component.value).toHaveLength(3); - expect(component.value).toContain(competency3); + expect(component.selectedCompetencies).toHaveLength(3); + expect(component.selectedCompetencies).toContain(competency3); component.toggleCompetency(competency2); - expect(component.value).toHaveLength(2); - expect(component.value).not.toContain(competency2); + expect(component.selectedCompetencies).toHaveLength(2); + expect(component.selectedCompetencies).not.toContain(competency2); component.toggleCompetency(competency1); component.toggleCompetency(competency3); - expect(component.value).toBeUndefined(); + expect(component.selectedCompetencies).toBeUndefined(); }); it('should register onchange', () => { From ac23a6c6e0759fd3ecc2a35bdd829ba255239a9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20St=C3=B6hr?= Date: Tue, 2 Jul 2024 19:26:33 +0200 Subject: [PATCH 04/11] Fix more tests --- .../competency-management/competency-management.component.ts | 1 - src/main/webapp/app/course/competencies/competency.service.ts | 1 - .../in/www1/artemis/lecture/PrerequisiteIntegrationTest.java | 2 +- .../component/competencies/create-competency.component.spec.ts | 2 +- 4 files changed, 2 insertions(+), 4 deletions(-) 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 54ac3f2b8afe..aa56373f4d03 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 @@ -230,7 +230,6 @@ export class CompetencyManagementComponent implements OnInit, OnDestroy { * @param relation the given competency relation */ createRelation(relation: CompetencyRelation) { - console.log(relation); this.competencyService .createCompetencyRelation(relation, this.courseId) .pipe( diff --git a/src/main/webapp/app/course/competencies/competency.service.ts b/src/main/webapp/app/course/competencies/competency.service.ts index 2f2103f73cfa..81a1bbb6a67a 100644 --- a/src/main/webapp/app/course/competencies/competency.service.ts +++ b/src/main/webapp/app/course/competencies/competency.service.ts @@ -124,7 +124,6 @@ export class CompetencyService { create(competency: Competency, courseId: number): Observable { const copy = this.convertCompetencyFromClient(competency); - console.log(copy); return this.httpClient.post(`${this.resourceURL}/courses/${courseId}/competencies`, copy, { observe: 'response' }); } diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/PrerequisiteIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/PrerequisiteIntegrationTest.java index 7346fca823e0..afc94ea420a5 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/PrerequisiteIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/PrerequisiteIntegrationTest.java @@ -203,7 +203,7 @@ void shouldNotDeletePrerequisiteNotInCourse() throws Exception { var prerequisite = prerequisiteUtilService.createPrerequisite(course); // try to delete prerequisite in course2 - request.delete(url(course2.getId(), prerequisite.getId()), HttpStatus.NOT_FOUND); + request.delete(url(course2.getId(), prerequisite.getId()), HttpStatus.BAD_REQUEST); boolean exists = prerequisiteRepository.existsById(prerequisite.getId()); assertThat(exists).isTrue(); diff --git a/src/test/javascript/spec/component/competencies/create-competency.component.spec.ts b/src/test/javascript/spec/component/competencies/create-competency.component.spec.ts index b11fc438fb94..d2737cbf43ba 100644 --- a/src/test/javascript/spec/component/competencies/create-competency.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/create-competency.component.spec.ts @@ -126,7 +126,7 @@ describe('CreateCompetency', () => { competencyForm.formSubmitted.emit(formData); return createCompetencyComponentFixture.whenStable().then(() => { - const competency: Competency = {}; + const competency: Competency = new Competency(); competency.title = formData.title; competency.description = formData.description; competency.optional = formData.optional; From 39f81c102c69d54ed5ac88ab3dc4e6ed04148869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20St=C3=B6hr?= Date: Tue, 2 Jul 2024 20:04:52 +0200 Subject: [PATCH 05/11] Add client test --- .../course-competency.service.spec.ts | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/test/javascript/spec/component/competencies/course-competency.service.spec.ts diff --git a/src/test/javascript/spec/component/competencies/course-competency.service.spec.ts b/src/test/javascript/spec/component/competencies/course-competency.service.spec.ts new file mode 100644 index 000000000000..a03d348aff0d --- /dev/null +++ b/src/test/javascript/spec/component/competencies/course-competency.service.spec.ts @@ -0,0 +1,58 @@ +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { LectureUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/lectureUnit.service'; +import { MockProvider } from 'ng-mocks'; +import { LectureUnit } from 'app/entities/lecture-unit/lectureUnit.model'; +import { CompetencyService } from 'app/course/competencies/competency.service'; +import { Competency } from 'app/entities/competency.model'; +import { AccountService } from 'app/core/auth/account.service'; +import { MockAccountService } from '../../helpers/mocks/service/mock-account.service'; +import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; +import { MockExerciseService } from '../../helpers/mocks/service/mock-exercise.service'; + +describe('CourseCompetencyService', () => { + let courseCompetencyService: CourseCompetencyService; + let httpTestingController: HttpTestingController; + let defaultCompetencies: Competency[]; + let expectedResultCompetency: any; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + MockProvider(LectureUnitService, { + convertLectureUnitArrayDatesFromServer(res: T[]): T[] { + return res; + }, + convertLectureUnitArrayDatesFromClient(lectureUnits: T[]): T[] { + return lectureUnits; + }, + }), + { provide: AccountService, useClass: MockAccountService }, + { provide: ExerciseService, useClass: MockExerciseService }, + ], + }); + expectedResultCompetency = {} as HttpResponse; + + courseCompetencyService = TestBed.inject(CompetencyService); + httpTestingController = TestBed.inject(HttpTestingController); + + defaultCompetencies = [{ id: 0, title: 'title', description: 'description' } as Competency]; + }); + + afterEach(() => { + httpTestingController.verify(); + }); + + it('should find all competencies', fakeAsync(() => { + const returnedFromService = [...defaultCompetencies]; + courseCompetencyService.getAllForCourse(1).subscribe((resp) => (expectedResultCompetency = resp)); + + const req = httpTestingController.expectOne({ method: 'GET' }); + req.flush(returnedFromService); + tick(); + + expect(expectedResultCompetency.body).toEqual(defaultCompetencies); + })); +}); From 1721df919ffc2a83b7104992c6f2fcb913557d1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20St=C3=B6hr?= Date: Tue, 2 Jul 2024 22:16:15 +0200 Subject: [PATCH 06/11] Fix client test and prerequisite utility --- .../LearningPathRecommendationService.java | 14 ++++++++++++++ .../competencies/course-competency.service.spec.ts | 11 +++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathRecommendationService.java b/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathRecommendationService.java index 0effd8f01370..fa1983350d04 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathRecommendationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathRecommendationService.java @@ -29,6 +29,7 @@ import de.tum.in.www1.artemis.domain.competency.CompetencyProgress; import de.tum.in.www1.artemis.domain.competency.CourseCompetency; import de.tum.in.www1.artemis.domain.competency.LearningPath; +import de.tum.in.www1.artemis.domain.competency.Prerequisite; import de.tum.in.www1.artemis.domain.competency.RelationType; import de.tum.in.www1.artemis.domain.enumeration.DifficultyLevel; import de.tum.in.www1.artemis.domain.lecture.LectureUnit; @@ -83,6 +84,8 @@ public class LearningPathRecommendationService { */ private static final double MASTERY_PROGRESS_UTILITY = 1; + private static final double PREREQUISITE_UTILITY = 200; + /** * Lookup table containing the distribution of exercises by difficulty level that should be recommended. *

@@ -381,6 +384,7 @@ private double computeUtilityOfCompetency(CourseCompetency competency, Recommend utility += computePriorUtility(competency, state); utility += computeExtendsOrAssumesUtility(competency, state); utility += computeMasteryUtility(competency, state); + utility += computePrerequisiteUtility(competency); return utility; } @@ -468,6 +472,16 @@ private static double computeMasteryUtility(CourseCompetency competency, Recomme return state.competencyMastery.get(competency.getId()) * MASTERY_PROGRESS_UTILITY; } + /** + * Gets the utility of the competency with respect to it being a prerequisite or not. + * + * @param competency the competency for which the utility should be computed + * @return prerequisite utility of the competency + */ + private static double computePrerequisiteUtility(CourseCompetency competency) { + return competency instanceof Prerequisite ? PREREQUISITE_UTILITY : 0; + } + /** * Analyzes the current progress within the learning path and generates a recommended ordering of uncompleted learning objects in a competency. * diff --git a/src/test/javascript/spec/component/competencies/course-competency.service.spec.ts b/src/test/javascript/spec/component/competencies/course-competency.service.spec.ts index a03d348aff0d..d1599b8bcfbf 100644 --- a/src/test/javascript/spec/component/competencies/course-competency.service.spec.ts +++ b/src/test/javascript/spec/component/competencies/course-competency.service.spec.ts @@ -4,12 +4,12 @@ import { TestBed, fakeAsync, tick } from '@angular/core/testing'; import { LectureUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/lectureUnit.service'; import { MockProvider } from 'ng-mocks'; import { LectureUnit } from 'app/entities/lecture-unit/lectureUnit.model'; -import { CompetencyService } from 'app/course/competencies/competency.service'; -import { Competency } from 'app/entities/competency.model'; +import { Competency, CourseCompetency, CourseCompetencyType } from 'app/entities/competency.model'; import { AccountService } from 'app/core/auth/account.service'; import { MockAccountService } from '../../helpers/mocks/service/mock-account.service'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { MockExerciseService } from '../../helpers/mocks/service/mock-exercise.service'; +import { CourseCompetencyService } from 'app/course/competencies/course-competency.service'; describe('CourseCompetencyService', () => { let courseCompetencyService: CourseCompetencyService; @@ -35,10 +35,13 @@ describe('CourseCompetencyService', () => { }); expectedResultCompetency = {} as HttpResponse; - courseCompetencyService = TestBed.inject(CompetencyService); + courseCompetencyService = TestBed.inject(CourseCompetencyService); httpTestingController = TestBed.inject(HttpTestingController); - defaultCompetencies = [{ id: 0, title: 'title', description: 'description' } as Competency]; + defaultCompetencies = [ + { id: 0, title: 'title', description: 'description', type: CourseCompetencyType.COMPETENCY } as CourseCompetency, + { id: 1, title: 'title2', description: 'description2', type: CourseCompetencyType.PREREQUISITE } as CourseCompetency, + ]; }); afterEach(() => { From 87fc6f351e13a0dbf3d5a8bffb93be9248cc8ebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20St=C3=B6hr?= Date: Tue, 2 Jul 2024 22:49:13 +0200 Subject: [PATCH 07/11] Add server tests --- .../CompetencyIntegrationTest.java | 7 +- .../CourseCompetencyIntegrationTest.java | 215 ++++++++++++++++++ 2 files changed, 217 insertions(+), 5 deletions(-) rename src/test/java/de/tum/in/www1/artemis/{lecture => competency}/CompetencyIntegrationTest.java (99%) create mode 100644 src/test/java/de/tum/in/www1/artemis/competency/CourseCompetencyIntegrationTest.java diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/CompetencyIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/competency/CompetencyIntegrationTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/lecture/CompetencyIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/competency/CompetencyIntegrationTest.java index b9db43c15f18..7716a1ebdede 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/CompetencyIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/competency/CompetencyIntegrationTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.lecture; +package de.tum.in.www1.artemis.competency; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.within; @@ -25,10 +25,6 @@ import de.tum.in.www1.artemis.AbstractSpringIntegrationLocalCILocalVCTest; import de.tum.in.www1.artemis.StudentScoreUtilService; -import de.tum.in.www1.artemis.competency.CompetencyProgressUtilService; -import de.tum.in.www1.artemis.competency.CompetencyUtilService; -import de.tum.in.www1.artemis.competency.PrerequisiteUtilService; -import de.tum.in.www1.artemis.competency.StandardizedCompetencyUtilService; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.DomainObject; @@ -59,6 +55,7 @@ import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseFactory; import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; +import de.tum.in.www1.artemis.lecture.LectureUtilService; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.AttachmentUnitRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/competency/CourseCompetencyIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/competency/CourseCompetencyIntegrationTest.java new file mode 100644 index 000000000000..6faccfcc33b2 --- /dev/null +++ b/src/test/java/de/tum/in/www1/artemis/competency/CourseCompetencyIntegrationTest.java @@ -0,0 +1,215 @@ +package de.tum.in.www1.artemis.competency; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.ZonedDateTime; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +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 org.springframework.http.HttpStatus; +import org.springframework.security.test.context.support.WithMockUser; + +import de.tum.in.www1.artemis.AbstractSpringIntegrationLocalCILocalVCTest; +import de.tum.in.www1.artemis.course.CourseUtilService; +import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.Lecture; +import de.tum.in.www1.artemis.domain.TextExercise; +import de.tum.in.www1.artemis.domain.competency.Competency; +import de.tum.in.www1.artemis.domain.competency.CompetencyTaxonomy; +import de.tum.in.www1.artemis.domain.competency.CourseCompetency; +import de.tum.in.www1.artemis.domain.competency.Prerequisite; +import de.tum.in.www1.artemis.domain.enumeration.ExerciseMode; +import de.tum.in.www1.artemis.domain.lecture.AttachmentUnit; +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.TextUnit; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; +import de.tum.in.www1.artemis.repository.AttachmentUnitRepository; +import de.tum.in.www1.artemis.repository.CompetencyRepository; +import de.tum.in.www1.artemis.repository.ExerciseRepository; +import de.tum.in.www1.artemis.repository.ExerciseUnitRepository; +import de.tum.in.www1.artemis.repository.LectureRepository; +import de.tum.in.www1.artemis.repository.PrerequisiteRepository; +import de.tum.in.www1.artemis.repository.TextUnitRepository; +import de.tum.in.www1.artemis.user.UserUtilService; + +class CourseCompetencyIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCTest { + + private static final String TEST_PREFIX = "coursecompetencyintegrationtest"; + + @Autowired + private LectureRepository lectureRepository; + + @Autowired + private ExerciseRepository exerciseRepository; + + @Autowired + private TextUnitRepository textUnitRepository; + + @Autowired + private AttachmentUnitRepository attachmentUnitRepository; + + @Autowired + private ExerciseUnitRepository exerciseUnitRepository; + + @Autowired + private CompetencyRepository competencyRepository; + + @Autowired + private UserUtilService userUtilService; + + @Autowired + private CourseUtilService courseUtilService; + + @Autowired + private PrerequisiteRepository prerequisiteRepository; + + private Course course; + + private Competency competency; + + private Prerequisite prerequisite; + + private Lecture lecture; + + private TextExercise teamTextExercise; + + private TextExercise textExercise; + + @BeforeEach + void setupTestScenario() { + participantScoreScheduleService.activate(); + + ZonedDateTime pastTimestamp = ZonedDateTime.now().minusDays(5); + userUtilService.addUsers(TEST_PREFIX, 2, 1, 1, 1); + + // Add users that are not in the course + userUtilService.createAndSaveUser(TEST_PREFIX + "student42"); + userUtilService.createAndSaveUser(TEST_PREFIX + "instructor42"); + + // creating course + course = courseUtilService.createCourse(); + + competency = createCompetency(course); + prerequisite = createPrerequisite(course); + lecture = createLecture(course); + + textExercise = createTextExercise(pastTimestamp, pastTimestamp, pastTimestamp, Set.of(competency), false); + teamTextExercise = createTextExercise(pastTimestamp, pastTimestamp, pastTimestamp, Set.of(prerequisite), true); + + creatingLectureUnitsOfLecture(competency); + creatingLectureUnitsOfLecture(prerequisite); + } + + private Competency createCompetency(Course course) { + Competency competency = new Competency(); + competency.setTitle("Competency" + course.getId()); + competency.setDescription("This is an example competency"); + competency.setTaxonomy(CompetencyTaxonomy.UNDERSTAND); + competency.setCourse(course); + competency = competencyRepository.save(competency); + + return competency; + } + + private Prerequisite createPrerequisite(Course course) { + Prerequisite prerequisite = new Prerequisite(); + prerequisite.setTitle("Prerequisite" + course.getId()); + prerequisite.setDescription("This is an example prerequisite"); + prerequisite.setTaxonomy(CompetencyTaxonomy.UNDERSTAND); + prerequisite.setCourse(course); + prerequisite = prerequisiteRepository.save(prerequisite); + + return prerequisite; + } + + private void creatingLectureUnitsOfLecture(CourseCompetency competency) { + // creating lecture units for lecture one + + TextUnit textUnit = new TextUnit(); + textUnit.setName("TextUnitOfLectureOne"); + textUnit.setCompetencies(Set.of(competency)); + textUnit = textUnitRepository.save(textUnit); + + AttachmentUnit attachmentUnit = new AttachmentUnit(); + attachmentUnit.setName("AttachmentUnitOfLectureOne"); + attachmentUnit.setCompetencies(Set.of(competency)); + attachmentUnit = attachmentUnitRepository.save(attachmentUnit); + + ExerciseUnit textExerciseUnit = new ExerciseUnit(); + textExerciseUnit.setExercise(textExercise); + exerciseUnitRepository.save(textExerciseUnit); + + ExerciseUnit teamTextExerciseUnit = new ExerciseUnit(); + teamTextExerciseUnit.setExercise(teamTextExercise); + exerciseUnitRepository.save(teamTextExerciseUnit); + + for (LectureUnit lectureUnit : List.of(textUnit, attachmentUnit, textExerciseUnit, teamTextExerciseUnit)) { + lecture.addLectureUnit(lectureUnit); + } + + lectureRepository.save(lecture); + } + + private Lecture createLecture(Course course) { + Lecture lecture = new Lecture(); + lecture.setTitle("LectureOne"); + lecture.setCourse(course); + lectureRepository.save(lecture); + + return lecture; + } + + private TextExercise createTextExercise(ZonedDateTime releaseDate, ZonedDateTime dueDate, ZonedDateTime assassmentDueDate, Set competencies, + boolean isTeamExercise) { + // creating text exercise with Result + TextExercise textExercise = TextExerciseFactory.generateTextExercise(releaseDate, dueDate, assassmentDueDate, course); + + if (isTeamExercise) { + textExercise.setMode(ExerciseMode.TEAM); + } + + textExercise.setMaxPoints(10.0); + textExercise.setBonusPoints(0.0); + textExercise.setCompetencies(competencies); + + return exerciseRepository.save(textExercise); + } + + @Nested + class GetCompetenciesOfCourse { + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void shouldReturnCompetenciesForStudentOfCourse() throws Exception { + TextUnit unreleasedLectureUnit = new TextUnit(); + unreleasedLectureUnit.setName("TextUnitOfLectureOne"); + unreleasedLectureUnit.setReleaseDate(ZonedDateTime.now().plusDays(5)); + unreleasedLectureUnit = textUnitRepository.save(unreleasedLectureUnit); + lecture.addLectureUnit(unreleasedLectureUnit); + lectureRepository.save(lecture); + + Competency newCompetency = new Competency(); + newCompetency.setTitle("Title"); + newCompetency.setDescription("Description"); + newCompetency.setCourse(course); + newCompetency.setLectureUnits(new HashSet<>(List.of(unreleasedLectureUnit))); + newCompetency = competencyRepository.save(newCompetency); + + List competenciesOfCourse = request.getList("/api/courses/" + course.getId() + "/course-competencies", HttpStatus.OK, CourseCompetency.class); + + assertThat(competenciesOfCourse).map(CourseCompetency::getId).containsExactlyInAnyOrder(competency.getId(), prerequisite.getId(), newCompetency.getId()); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student42", roles = "USER") + void testShouldReturnForbiddenForStudentNotInCourse() throws Exception { + request.getList("/api/courses/" + course.getId() + "/course-competencies", HttpStatus.FORBIDDEN, Competency.class); + } + } +} From 3cf635aab97a904a927e779b3ad91cbdc04a3312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20St=C3=B6hr?= Date: Tue, 2 Jul 2024 23:22:44 +0200 Subject: [PATCH 08/11] Fix server style --- .../in/www1/artemis/service/competency/PrerequisiteService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/de/tum/in/www1/artemis/service/competency/PrerequisiteService.java b/src/main/java/de/tum/in/www1/artemis/service/competency/PrerequisiteService.java index 267d3c4b8c72..13b9e0289a58 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/competency/PrerequisiteService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/competency/PrerequisiteService.java @@ -106,6 +106,7 @@ public Prerequisite updatePrerequisite(PrerequisiteRequestDTO prerequisiteValues * * @param courseId the course to import into * @param courseCompetencyIds the ids of the courseCompetencies to import + * @param relations the relations between the prerequisites to import * @return The list of imported prerequisites */ public List importPrerequisites(long courseId, List courseCompetencyIds, Set relations) { From 54683ce3693cf5d6a231afd5310ecfad0bd04f2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20St=C3=B6hr?= Date: Thu, 4 Jul 2024 00:10:08 +0200 Subject: [PATCH 09/11] Patrik --- .../rest/competency/CompetencyResource.java | 18 +++++++++--------- .../rest/competency/PrerequisiteResource.java | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/competency/CompetencyResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/competency/CompetencyResource.java index 76fcc1636d65..5ee5bb9c5808 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/competency/CompetencyResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/competency/CompetencyResource.java @@ -193,7 +193,7 @@ public ResponseEntity getCompetency(@PathVariable long competencyId, var currentUser = userRepository.getUserWithGroupsAndAuthorities(); var course = courseRepository.findByIdElseThrow(courseId); var competency = competencyService.findCompetencyWithExercisesAndLectureUnitsAndProgressForUser(competencyId, currentUser.getId()); - checkAuthorizationForCompetency(course, competency); + checkCourseForCompetency(course, competency); competency.setLectureUnits(competency.getLectureUnits().stream().filter(lectureUnit -> authorizationCheckService.isAllowedToSeeLectureUnit(lectureUnit, currentUser)) .peek(lectureUnit -> lectureUnit.setCompleted(lectureUnit.isCompletedFor(currentUser))).collect(Collectors.toSet())); @@ -223,7 +223,7 @@ public ResponseEntity updateCompetency(@PathVariable long courseId, } var course = courseRepository.findByIdElseThrow(courseId); var existingCompetency = competencyRepository.findByIdWithLectureUnitsElseThrow(competency.getId()); - checkAuthorizationForCompetency(course, existingCompetency); + checkCourseForCompetency(course, existingCompetency); var persistedCompetency = competencyService.updateCompetency(existingCompetency, competency); lectureUnitService.linkLectureUnitsToCompetency(persistedCompetency, competency.getLectureUnits(), existingCompetency.getLectureUnits()); @@ -403,7 +403,7 @@ public ResponseEntity deleteCompetency(@PathVariable long competencyId, @P var course = courseRepository.findByIdElseThrow(courseId); var competency = courseCompetencyRepository.findByIdWithExercisesAndLectureUnitsBidirectionalElseThrow(competencyId); - checkAuthorizationForCompetency(course, competency); + checkCourseForCompetency(course, competency); courseCompetencyService.deleteCourseCompetency(competency, course); @@ -426,7 +426,7 @@ public ResponseEntity getCompetencyStudentProgress(@PathVari var user = userRepository.getUserWithGroupsAndAuthorities(); var course = courseRepository.findByIdElseThrow(courseId); var competency = competencyRepository.findByIdElseThrow(competencyId); - checkAuthorizationForCompetency(course, competency); + checkCourseForCompetency(course, competency); CompetencyProgress studentProgress; if (refresh) { @@ -493,9 +493,9 @@ public ResponseEntity createCompetencyRelation(@PathVaria var course = courseRepository.findByIdElseThrow(courseId); var tailCompetency = courseCompetencyRepository.findByIdElseThrow(tailId); - checkAuthorizationForCompetency(course, tailCompetency); + checkCourseForCompetency(course, tailCompetency); var headCompetency = courseCompetencyRepository.findByIdElseThrow(headId); - checkAuthorizationForCompetency(course, headCompetency); + checkCourseForCompetency(course, headCompetency); var createdRelation = competencyRelationService.createCompetencyRelation(tailCompetency, headCompetency, relation.relationType(), course); @@ -516,8 +516,8 @@ public ResponseEntity removeCompetencyRelation(@PathVariable long courseId var course = courseRepository.findByIdElseThrow(courseId); var relation = competencyRelationRepository.findById(competencyRelationId).orElseThrow(); - checkAuthorizationForCompetency(course, relation.getTailCompetency()); - checkAuthorizationForCompetency(course, relation.getHeadCompetency()); + checkCourseForCompetency(course, relation.getTailCompetency()); + checkCourseForCompetency(course, relation.getHeadCompetency()); competencyRelationRepository.delete(relation); @@ -564,7 +564,7 @@ public ResponseEntity> getCourseCompetencyTitles(@PathVariable Long * @param course The course for which to check the authorization role for * @param competency The competency to be accessed by the user */ - private void checkAuthorizationForCompetency(@NotNull Course course, @NotNull CourseCompetency competency) { + private void checkCourseForCompetency(@NotNull Course course, @NotNull CourseCompetency competency) { if (competency.getCourse() == null) { throw new BadRequestAlertException("A competency must belong to a course", ENTITY_NAME, "competencyNoCourse"); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/competency/PrerequisiteResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/competency/PrerequisiteResource.java index e9d34c823fa9..0208570b32f7 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/competency/PrerequisiteResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/competency/PrerequisiteResource.java @@ -164,7 +164,7 @@ public ResponseEntity deletePrerequisite(@PathVariable long prerequisiteId var course = courseRepository.findByIdElseThrow(courseId); var prerequisite = courseCompetencyRepository.findByIdWithExercisesAndLectureUnitsBidirectionalElseThrow(prerequisiteId); - checkAuthorizationForPrerequisite(course, prerequisite); + checkCourseForPrerequisite(course, prerequisite); courseCompetencyService.deleteCourseCompetency(prerequisite, course); return ResponseEntity.ok().build(); @@ -196,7 +196,7 @@ public ResponseEntity> importPrerequisites(@PathVa * @param course The course for which to check the authorization role for * @param prerequisite The prerequisite to be accessed by the user */ - private void checkAuthorizationForPrerequisite(@NotNull Course course, @NotNull CourseCompetency prerequisite) { + private void checkCourseForPrerequisite(@NotNull Course course, @NotNull CourseCompetency prerequisite) { if (prerequisite.getCourse() == null) { throw new BadRequestAlertException("A competency must belong to a course", ENTITY_NAME, "competencyNoCourse"); } From 0e5e0da52ff7201c1a5ec2262913a9e172f4abc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20St=C3=B6hr?= Date: Thu, 4 Jul 2024 16:29:51 +0200 Subject: [PATCH 10/11] Fix reviews --- .../competency-management/competency-management.component.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 aa56373f4d03..40becdb660f1 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 @@ -117,6 +117,7 @@ export class CompetencyManagementComponent implements OnInit, OnDestroy { this.alertService.success('artemisApp.prerequisite.manage.deleted'); this.prerequisites = this.prerequisites.filter((prerequisite) => prerequisite.id !== prerequisiteId); this.courseCompetencies = [...this.competencies, ...this.prerequisites]; + this.relations = this.relations.filter((relation) => relation.tailCompetency?.id !== prerequisiteId && relation.headCompetency?.id !== prerequisiteId); this.dialogErrorSource.next(''); }, error: (error: HttpErrorResponse) => onError(this.alertService, error), @@ -255,8 +256,8 @@ export class CompetencyManagementComponent implements OnInit, OnDestroy { const relation = this.relations.find((relation) => relation.id === relationId); const headId = relation?.headCompetency?.id; const tailId = relation?.tailCompetency?.id; - const titleHead = this.competencies.find((competency) => competency.id === headId)?.title ?? ''; - const titleTail = this.competencies.find((competency) => competency.id === tailId)?.title ?? ''; + const titleHead = this.courseCompetencies.find((competency) => competency.id === headId)?.title ?? ''; + const titleTail = this.courseCompetencies.find((competency) => competency.id === tailId)?.title ?? ''; const modalRef = this.modalService.open(ConfirmAutofocusModalComponent, { keyboard: true, size: 'md' }); modalRef.componentInstance.title = 'artemisApp.competency.manageCompetencies.deleteRelationModalTitle'; From f6117c0e68b4943ec045851236b6d40a0e89c825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20St=C3=B6hr?= Date: Fri, 12 Jul 2024 17:47:55 +0200 Subject: [PATCH 11/11] Fix CourseCompetency vs. Competency --- .../LearningPathNavigationService.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathNavigationService.java b/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathNavigationService.java index 4972ca6e0969..c890f29fdf55 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathNavigationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathNavigationService.java @@ -46,7 +46,7 @@ public LearningPathNavigationService(LearningPathRecommendationService learningP public LearningPathNavigationDTO getNavigation(LearningPath learningPath) { var recommendationState = learningPathRecommendationService.getRecommendedOrderOfNotMasteredCompetencies(learningPath); var currentLearningObject = learningPathRecommendationService.getFirstLearningObject(learningPath.getUser(), recommendationState); - Competency competencyOfCurrentLearningObject; + CourseCompetency competencyOfCurrentLearningObject; var recommendationStateWithAllCompetencies = learningPathRecommendationService.getRecommendedOrderOfAllCompetencies(learningPath); // If all competencies are mastered, get the last completed learning object @@ -77,13 +77,13 @@ public LearningPathNavigationDTO getNavigation(LearningPath learningPath) { * @param firstCompetency whether to find the first or last competency that contains the learning object * @return the competency that contains the learning object */ - private Competency findCorrespondingCompetencyForLearningObject(RecommendationState recommendationState, LearningObject learningObject, boolean firstCompetency) { - Stream potentialCompetencies = recommendationState.recommendedOrderOfCompetencies().stream() + private CourseCompetency findCorrespondingCompetencyForLearningObject(RecommendationState recommendationState, LearningObject learningObject, boolean firstCompetency) { + Stream potentialCompetencies = recommendationState.recommendedOrderOfCompetencies().stream() .map(competencyId -> recommendationState.competencyIdMap().get(competencyId)) .filter(competency -> competency.getLectureUnits().contains(learningObject) || competency.getExercises().contains(learningObject)); // There will always be at least one competency that contains the learning object, otherwise the learning object would not be in the learning path - Comparator comparator = Comparator.comparingInt(competency -> recommendationState.recommendedOrderOfCompetencies().indexOf(competency.getId())); + Comparator comparator = Comparator.comparingInt(competency -> recommendationState.recommendedOrderOfCompetencies().indexOf(competency.getId())); if (firstCompetency) { return potentialCompetencies.min(comparator).get(); } @@ -128,7 +128,7 @@ private LearningPathNavigationDTO getNavigationRelativeToLearningObject(Recommen private LearningPathNavigationObjectDTO getPredecessorOfLearningObject(RecommendationState recommendationState, CourseCompetency currentCompetency, List learningObjectsInCurrentCompetency, int indexOfCurrentLearningObject, User user) { LearningObject predecessorLearningObject = null; - Competency competencyOfPredecessor = null; + CourseCompetency competencyOfPredecessor = null; if (indexOfCurrentLearningObject <= 0) { int indexOfCompetencyToSearch = recommendationState.recommendedOrderOfCompetencies().indexOf(currentCompetency.getId()) - 1; while (indexOfCompetencyToSearch >= 0 && predecessorLearningObject == null) { @@ -152,7 +152,7 @@ private LearningPathNavigationObjectDTO getPredecessorOfLearningObject(Recommend private LearningPathNavigationObjectDTO getSuccessorOfLearningObject(RecommendationState recommendationState, CourseCompetency currentCompetency, List learningObjectsInCurrentCompetency, int indexOfCurrentLearningObject, User user) { LearningObject successorLearningObject = null; - Competency competencyOfSuccessor = null; + CourseCompetency competencyOfSuccessor = null; if (indexOfCurrentLearningObject >= learningObjectsInCurrentCompetency.size() - 1) { int indexOfCompetencyToSearch = recommendationState.recommendedOrderOfCompetencies().indexOf(currentCompetency.getId()) + 1; while (indexOfCompetencyToSearch < recommendationState.recommendedOrderOfCompetencies().size() && successorLearningObject == null) { @@ -189,7 +189,7 @@ public LearningPathNavigationOverviewDTO getNavigationOverview(LearningPath lear return new LearningPathNavigationOverviewDTO(learningObjects); } - private LearningPathNavigationObjectDTO createLearningPathNavigationObjectDTO(LearningObject learningObject, User user, Competency competency) { + private LearningPathNavigationObjectDTO createLearningPathNavigationObjectDTO(LearningObject learningObject, User user, CourseCompetency competency) { if (learningObject == null) { return null; }