From afd2fdfbdf677ae36f4c5cf6ab2b8a081db34b5a Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Fri, 24 May 2024 10:21:28 +0200 Subject: [PATCH 01/78] Add database migration and basic entities --- .../domain/competency/AbstractCompetency.java | 131 ++++++++++++++++++ .../artemis/domain/competency/Competency.java | 107 +------------- .../domain/competency/Prerequisite.java | 9 ++ .../changelog/20240523180900_changelog.xml | 43 ++++++ .../resources/config/liquibase/master.xml | 1 + 5 files changed, 191 insertions(+), 100 deletions(-) create mode 100644 src/main/java/de/tum/in/www1/artemis/domain/competency/AbstractCompetency.java create mode 100644 src/main/java/de/tum/in/www1/artemis/domain/competency/Prerequisite.java create mode 100644 src/main/resources/config/liquibase/changelog/20240523180900_changelog.xml diff --git a/src/main/java/de/tum/in/www1/artemis/domain/competency/AbstractCompetency.java b/src/main/java/de/tum/in/www1/artemis/domain/competency/AbstractCompetency.java new file mode 100644 index 000000000000..fcf8077abebc --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/domain/competency/AbstractCompetency.java @@ -0,0 +1,131 @@ +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.Table; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.tum.in.www1.artemis.domain.Course; + +@Entity +@Table(name = "competency") +@Inheritance(strategy = InheritanceType.SINGLE_TABLE) +@DiscriminatorColumn(name = "discriminator", discriminatorType = DiscriminatorType.STRING) +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) +public abstract class AbstractCompetency extends BaseCompetency { + + @JsonIgnore + public static final int DEFAULT_MASTERY_THRESHOLD = 50; + + @Column(name = "soft_due_date") + private ZonedDateTime softDueDate; + + @Column(name = "mastery_threshold") + private Integer masteryThreshold; + + @Column(name = "optional") + private boolean optional; + + @ManyToOne + @JoinColumn(name = "course_id") + @JsonIgnoreProperties({ "competencies", "prerequisites" }) + private Course course; + + @ManyToOne + @JoinColumn(name = "linked_standardized_competency_id") + @JsonIgnoreProperties({ "competencies" }) + private StandardizedCompetency linkedStandardizedCompetency; + + @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<>(); + + public AbstractCompetency() { + } + + public AbstractCompetency(String title, String description, ZonedDateTime softDueDate, Integer masteryThreshold, CompetencyTaxonomy taxonomy, boolean optional) { + super(title, description, taxonomy); + this.softDueDate = softDueDate; + this.masteryThreshold = masteryThreshold; + this.optional = optional; + } + + public ZonedDateTime getSoftDueDate() { + return softDueDate; + } + + public void setSoftDueDate(ZonedDateTime softDueDate) { + this.softDueDate = softDueDate; + } + + public Integer getMasteryThreshold() { + return masteryThreshold; + } + + public void setMasteryThreshold(Integer masteryThreshold) { + this.masteryThreshold = masteryThreshold; + } + + public boolean isOptional() { + return optional; + } + + public void setOptional(boolean optional) { + this.optional = optional; + } + + public Course getCourse() { + return course; + } + + public void setCourse(Course course) { + this.course = course; + } + + public StandardizedCompetency getLinkedStandardizedCompetency() { + return linkedStandardizedCompetency; + } + + public void setLinkedStandardizedCompetency(StandardizedCompetency linkedStandardizedCompetency) { + this.linkedStandardizedCompetency = linkedStandardizedCompetency; + } + + 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; + } +} 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 0c5ef1f44bb4..e791ba55b3ad 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 @@ -4,23 +4,17 @@ import java.util.HashSet; import java.util.Set; -import jakarta.persistence.CascadeType; -import jakarta.persistence.Column; +import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinTable; 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; import org.hibernate.annotations.CacheConcurrencyStrategy; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import de.tum.in.www1.artemis.domain.Course; @@ -29,37 +23,22 @@ import de.tum.in.www1.artemis.domain.lecture.LectureUnit; @Entity -@Table(name = "competency") -@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) -public class Competency extends BaseCompetency { - - @JsonIgnore - public static final int DEFAULT_MASTERY_THRESHOLD = 50; - - @Column(name = "soft_due_date") - private ZonedDateTime softDueDate; - - @Column(name = "mastery_threshold") - private Integer masteryThreshold; - - @Column(name = "optional") - private boolean optional; - - @ManyToOne - @JoinColumn(name = "course_id") - @JsonIgnoreProperties({ "competencies", "prerequisites" }) - private Course course; +@DiscriminatorValue("COMPETENCY") +public class Competency extends AbstractCompetency { + // TODO: move to AbstractCompetency @ManyToMany(mappedBy = "competencies") @JsonIgnoreProperties({ "competencies", "course" }) private Set exercises = new HashSet<>(); + // TODO: move to AbstractCompetency @ManyToMany(mappedBy = "competencies") @JsonIgnoreProperties("competencies") private Set lectureUnits = new HashSet<>(); /** * A set of courses for which this competency is a prerequisite for. + * TODO: remove this once the prerequisite migration is complete */ @ManyToMany @JoinTable(name = "competency_course", joinColumns = @JoinColumn(name = "competency_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "course_id", referencedColumnName = "id")) @@ -67,59 +46,11 @@ public class Competency extends BaseCompetency { @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) private Set consecutiveCourses = 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 = "linked_standardized_competency_id") - @JsonIgnoreProperties({ "competencies" }) - private StandardizedCompetency linkedStandardizedCompetency; - public Competency() { } public Competency(String title, String description, ZonedDateTime softDueDate, Integer masteryThreshold, CompetencyTaxonomy taxonomy, boolean optional) { - super(title, description, taxonomy); - this.softDueDate = softDueDate; - this.masteryThreshold = masteryThreshold; - this.optional = optional; - } - - public ZonedDateTime getSoftDueDate() { - return softDueDate; - } - - public void setSoftDueDate(ZonedDateTime dueDate) { - this.softDueDate = dueDate; - } - - public int getMasteryThreshold() { - return masteryThreshold == null ? 100 : this.masteryThreshold; - } - - public void setMasteryThreshold(Integer masteryThreshold) { - this.masteryThreshold = masteryThreshold; - } - - public boolean isOptional() { - return optional; - } - - public void setOptional(boolean optional) { - this.optional = optional; - } - - public Course getCourse() { - return course; - } - - public void setCourse(Course course) { - this.course = course; + super(title, description, softDueDate, masteryThreshold, taxonomy, optional); } public Set getExercises() { @@ -186,30 +117,6 @@ public void setConsecutiveCourses(Set consecutiveCourses) { this.consecutiveCourses = consecutiveCourses; } - 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; - } - - public StandardizedCompetency getLinkedStandardizedCompetency() { - return linkedStandardizedCompetency; - } - - public void setLinkedStandardizedCompetency(StandardizedCompetency linkedStandardizedCompetency) { - this.linkedStandardizedCompetency = linkedStandardizedCompetency; - } - /** * Ensure that exercise units are connected to competencies through the corresponding exercise */ diff --git a/src/main/java/de/tum/in/www1/artemis/domain/competency/Prerequisite.java b/src/main/java/de/tum/in/www1/artemis/domain/competency/Prerequisite.java new file mode 100644 index 000000000000..faa470b77199 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/domain/competency/Prerequisite.java @@ -0,0 +1,9 @@ +package de.tum.in.www1.artemis.domain.competency; + +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; + +@Entity +@DiscriminatorValue("PREREQUISITE") +public class Prerequisite extends AbstractCompetency { +} diff --git a/src/main/resources/config/liquibase/changelog/20240523180900_changelog.xml b/src/main/resources/config/liquibase/changelog/20240523180900_changelog.xml new file mode 100644 index 000000000000..d9a242d2d70b --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20240523180900_changelog.xml @@ -0,0 +1,43 @@ + + + + + + + UPDATE competency SET discriminator = 'COMPETENCY' + + + + + + + + + DO $$ + DECLARE max_id BIGINT; + BEGIN + max_id := (SELECT (MAX(id)) as id FROM competency); + INSERT INTO competency (id, description, title, course_id, taxonomy, mastery_threshold, soft_due_date, optional, linked_standardized_competency_id, discriminator) + SELECT (max_id + row_number() over ()) as id, description, title, competency_course.course_id AS course_id, taxonomy, mastery_threshold, soft_due_date, optional, linked_standardized_competency_id, 'PREREQUISITE' + FROM competency + RIGHT JOIN competency_course ON competency.id = competency_course.competency_id; + END $$; + + + + + + + + INSERT INTO competency (id, description, title, course_id, taxonomy, mastery_threshold, soft_due_date, optional, linked_standardized_competency_id, discriminator) + SELECT @max_id := @max_id + 1 as id, description, title, competency_course.course_id as course_id, taxonomy, mastery_threshold, soft_due_date, optional, linked_standardized_competency_id, 'PREREQUISITE' + FROM competency + RIGHT JOIN competency_course on competency.id = competency_course.competency_id, (SELECT @max_id := MAX(id) FROM competency) max_id_table; + + + + + + diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index 94a99791bbfc..75bbea9e766b 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -10,6 +10,7 @@ + From 656eddd80c1a79c60074b0147e9648181c35972a Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Fri, 24 May 2024 12:16:23 +0200 Subject: [PATCH 02/78] Make changeset more readable, add comments and fix small MySQL bug --- .../changelog/20240523180900_changelog.xml | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/main/resources/config/liquibase/changelog/20240523180900_changelog.xml b/src/main/resources/config/liquibase/changelog/20240523180900_changelog.xml index d9a242d2d70b..25e71d13437c 100644 --- a/src/main/resources/config/liquibase/changelog/20240523180900_changelog.xml +++ b/src/main/resources/config/liquibase/changelog/20240523180900_changelog.xml @@ -7,13 +7,24 @@ UPDATE competency SET discriminator = 'COMPETENCY' - - + + + + + DO $$ DECLARE max_id BIGINT; @@ -30,11 +41,13 @@ + + SELECT @max_id := MAX(id) FROM competency; INSERT INTO competency (id, description, title, course_id, taxonomy, mastery_threshold, soft_due_date, optional, linked_standardized_competency_id, discriminator) - SELECT @max_id := @max_id + 1 as id, description, title, competency_course.course_id as course_id, taxonomy, mastery_threshold, soft_due_date, optional, linked_standardized_competency_id, 'PREREQUISITE' + SELECT (@max_id := @max_id + 1) as id, description, title, competency_course.course_id as course_id, taxonomy, mastery_threshold, soft_due_date, optional, linked_standardized_competency_id, 'PREREQUISITE' FROM competency - RIGHT JOIN competency_course on competency.id = competency_course.competency_id, (SELECT @max_id := MAX(id) FROM competency) max_id_table; + RIGHT JOIN competency_course on competency.id = competency_course.competency_id; From 0d29b03f9139d02d2d83fed2ecf765ace20e2ce1 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Fri, 24 May 2024 13:18:59 +0200 Subject: [PATCH 03/78] Remove all occurrences of prerequisites in the server code --- .../de/tum/in/www1/artemis/domain/Course.java | 23 +++++---------- .../artemis/domain/competency/Competency.java | 24 --------------- .../repository/CompetencyRepository.java | 29 ------------------- .../www1/artemis/service/CourseService.java | 3 +- .../service/competency/CompetencyService.java | 4 ++- .../artemis/web/rest/CompetencyResource.java | 21 +++++++++----- 6 files changed, 25 insertions(+), 79 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Course.java b/src/main/java/de/tum/in/www1/artemis/domain/Course.java index eabd67f9d8de..fe36970a89ca 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Course.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Course.java @@ -36,6 +36,7 @@ import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.competency.LearningPath; +import de.tum.in.www1.artemis.domain.competency.Prerequisite; import de.tum.in.www1.artemis.domain.enumeration.CourseInformationSharingConfiguration; import de.tum.in.www1.artemis.domain.enumeration.Language; import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; @@ -246,10 +247,10 @@ public class Course extends DomainObject { private Set organizations = new HashSet<>(); @ManyToMany - @JoinTable(name = "competency_course", joinColumns = @JoinColumn(name = "course_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "competency_id", referencedColumnName = "id")) - @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) - @JsonIgnoreProperties("consecutiveCourses") - private Set prerequisites = new HashSet<>(); + @OneToMany(mappedBy = "course", cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.LAZY) + @JsonIgnoreProperties("course") + @OrderBy("title") + private Set prerequisites = new HashSet<>(); @OneToOne(cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.LAZY) @JoinColumn(name = "tutorial_groups_configuration_id") @@ -697,24 +698,14 @@ public void setOrganizations(Set organizations) { this.organizations = organizations; } - public Set getPrerequisites() { + public Set getPrerequisites() { return prerequisites; } - public void setPrerequisites(Set prerequisites) { + public void setPrerequisites(Set prerequisites) { this.prerequisites = prerequisites; } - public void addPrerequisite(Competency competency) { - this.prerequisites.add(competency); - competency.getConsecutiveCourses().add(this); - } - - public void removePrerequisite(Competency competency) { - this.prerequisites.remove(competency); - competency.getConsecutiveCourses().remove(this); - } - @Override public String toString() { return "Course{" + "id=" + getId() + ", title='" + getTitle() + "'" + ", description='" + getDescription() + "'" + ", shortName='" + getShortName() + "'" 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 e791ba55b3ad..b1f8373725b1 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 @@ -6,18 +6,12 @@ import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.JoinTable; import jakarta.persistence.ManyToMany; import jakarta.persistence.PrePersist; import jakarta.persistence.PreUpdate; -import org.hibernate.annotations.Cache; -import org.hibernate.annotations.CacheConcurrencyStrategy; - import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -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; @@ -36,16 +30,6 @@ public class Competency extends AbstractCompetency { @JsonIgnoreProperties("competencies") private Set lectureUnits = new HashSet<>(); - /** - * A set of courses for which this competency is a prerequisite for. - * TODO: remove this once the prerequisite migration is complete - */ - @ManyToMany - @JoinTable(name = "competency_course", joinColumns = @JoinColumn(name = "competency_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "course_id", referencedColumnName = "id")) - @JsonIgnoreProperties({ "competencies", "prerequisites" }) - @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) - private Set consecutiveCourses = new HashSet<>(); - public Competency() { } @@ -109,14 +93,6 @@ public void removeLectureUnit(LectureUnit lectureUnit) { lectureUnit.getCompetencies().remove(this); } - public Set getConsecutiveCourses() { - return consecutiveCourses; - } - - public void setConsecutiveCourses(Set consecutiveCourses) { - this.consecutiveCourses = consecutiveCourses; - } - /** * Ensure that exercise units are connected to competencies through the corresponding exercise */ 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 572c610b3f0d..09458bdc28eb 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 @@ -81,31 +81,6 @@ public interface CompetencyRepository extends JpaRepository, J """) Optional findByIdWithExercisesAndLectureUnitsBidirectional(@Param("competencyId") long competencyId); - @Query(""" - SELECT c - FROM Competency c - LEFT JOIN FETCH c.consecutiveCourses - WHERE c.id = :competencyId - """) - Optional findByIdWithConsecutiveCourses(@Param("competencyId") long competencyId); - - @Query(""" - SELECT pr - FROM Competency pr - LEFT JOIN pr.consecutiveCourses c - WHERE c.id = :courseId - ORDER BY pr.title - """) - Set findPrerequisitesByCourseId(@Param("courseId") long courseId); - - @Query(""" - SELECT COUNT(*) - FROM Competency pr - LEFT JOIN pr.consecutiveCourses c - WHERE c.id = :courseId - """) - Long countPrerequisitesByCourseId(@Param("courseId") long courseId); - /** * Query which fetches all competencies for which the user is editor or instructor in the course and * matching the search criteria. @@ -195,10 +170,6 @@ default Competency findByIdWithExercisesAndLectureUnitsBidirectionalElseThrow(lo return findByIdWithExercisesAndLectureUnitsBidirectional(competencyId).orElseThrow(() -> new EntityNotFoundException("Competency", competencyId)); } - default Competency findByIdWithConsecutiveCoursesElseThrow(long competencyId) { - return findByIdWithConsecutiveCourses(competencyId).orElseThrow(() -> new EntityNotFoundException("Competency", competencyId)); - } - default Competency findByIdElseThrow(long competencyId) { return findById(competencyId).orElseThrow(() -> new EntityNotFoundException("Competency", competencyId)); } 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 a86b28e6324a..2e2457210f09 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 @@ -321,7 +321,8 @@ public Course findOneWithExercisesAndLecturesAndExamsAndCompetenciesAndTutorialG // NOTE: in this call we only want to know if competencies exist in the course, we will load them when the user navigates into them course.setNumberOfCompetencies(competencyRepository.countByCourse(course)); // NOTE: in this call we only want to know if prerequisites exist in the course, we will load them when the user navigates into them - course.setNumberOfPrerequisites(competencyRepository.countPrerequisitesByCourseId(course.getId())); + // TODO: change this. + course.setNumberOfPrerequisites(0L); // NOTE: in this call we only want to know if tutorial groups exist in the course, we will load them when the user navigates into them course.setNumberOfTutorialGroups(tutorialGroupRepository.countByCourse(course)); if (authCheckService.isOnlyStudentInCourse(course, user)) { 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 ce5ff28f04ed..b2fea17d9d29 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 @@ -99,7 +99,9 @@ public CompetencyService(CompetencyRepository competencyRepository, Authorizatio * @return A list of prerequisites. */ public Set findAllPrerequisitesForCourse(@NotNull Course course) { - Set prerequisites = competencyRepository.findPrerequisitesByCourseId(course.getId()); + Set prerequisites = Set.of(); + // TODO: logic + // competencyRepository.findPrerequisitesByCourseId(course.getId()); // Remove all lecture units for (Competency prerequisite : prerequisites) { prerequisite.setLectureUnits(Collections.emptySet()); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java index 506555b97604..2a5965610bb8 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java @@ -567,7 +567,8 @@ public ResponseEntity> getPrerequisites(@PathVariable long cour public ResponseEntity addPrerequisite(@PathVariable long competencyId, @PathVariable long courseId) { log.info("REST request to add a prerequisite: {}", competencyId); var course = courseRepository.findWithEagerCompetenciesByIdElseThrow(courseId); - var competency = competencyRepository.findByIdWithConsecutiveCoursesElseThrow(competencyId); + // var competency = competencyRepository.findByIdWithConsecutiveCoursesElseThrow(competencyId); + var competency = new Competency(); authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null); authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, competency.getCourse(), null); @@ -575,8 +576,8 @@ public ResponseEntity addPrerequisite(@PathVariable long competencyI throw new BadRequestAlertException("The competency of a course can not be a prerequisite to the same course", ENTITY_NAME, "competencyCycle"); } - course.addPrerequisite(competency); - courseRepository.save(course); + // TODO: do notuse competencyId. + // TODO: add new logic. return ResponseEntity.ok().body(competency); } @@ -592,14 +593,18 @@ public ResponseEntity addPrerequisite(@PathVariable long competencyI public ResponseEntity removePrerequisite(@PathVariable long competencyId, @PathVariable long courseId) { log.info("REST request to remove a prerequisite: {}", competencyId); var course = courseRepository.findWithEagerCompetenciesByIdElseThrow(courseId); - var competency = competencyRepository.findByIdWithConsecutiveCoursesElseThrow(competencyId); - if (!competency.getConsecutiveCourses().stream().map(Course::getId).toList().contains(courseId)) { - throw new BadRequestAlertException("The competency is not a prerequisite of the given course", ENTITY_NAME, "prerequisiteWrongCourse"); - } + // var competency = competencyRepository.findByIdWithConsecutiveCoursesElseThrow(competencyId); + var competency = new Competency(); + // TODO: replace logic. + /* + * if (!competency.getConsecutiveCourses().stream().map(Course::getId).toList().contains(courseId)) { + * throw new BadRequestAlertException("The competency is not a prerequisite of the given course", ENTITY_NAME, "prerequisiteWrongCourse"); + * } + */ authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null); authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, competency.getCourse(), null); - course.removePrerequisite(competency); + // TODO: add new logic. courseRepository.save(course); return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, PREREQUISITE_NAME, competency.getTitle())).build(); From fc7eb1e40c34bf0624c8b9a0d357a1eefb5b2127 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Fri, 24 May 2024 15:26:47 +0200 Subject: [PATCH 04/78] Add Repository, Service and DTOs --- .../domain/competency/Prerequisite.java | 9 +++ .../repository/PrerequisiteRepository.java | 41 ++++++++++ .../competency/PrerequisiteService.java | 76 +++++++++++++++++++ .../competency/PrerequisiteRequestDTO.java | 20 +++++ .../competency/PrerequisiteResponseDTO.java | 20 +++++ 5 files changed, 166 insertions(+) create mode 100644 src/main/java/de/tum/in/www1/artemis/repository/PrerequisiteRepository.java create mode 100644 src/main/java/de/tum/in/www1/artemis/service/competency/PrerequisiteService.java create mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteRequestDTO.java create mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteResponseDTO.java 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 faa470b77199..8d6072baf53d 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 @@ -1,9 +1,18 @@ package de.tum.in.www1.artemis.domain.competency; +import java.time.ZonedDateTime; + import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; @Entity @DiscriminatorValue("PREREQUISITE") public class Prerequisite extends AbstractCompetency { + + public Prerequisite(String title, String description, ZonedDateTime softDueDate, Integer masteryThreshold, CompetencyTaxonomy taxonomy, boolean optional) { + super(title, description, softDueDate, masteryThreshold, taxonomy, optional); + } + + public Prerequisite() { + } } diff --git a/src/main/java/de/tum/in/www1/artemis/repository/PrerequisiteRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/PrerequisiteRepository.java new file mode 100644 index 000000000000..a885d6b297ac --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/repository/PrerequisiteRepository.java @@ -0,0 +1,41 @@ +package de.tum.in.www1.artemis.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import de.tum.in.www1.artemis.domain.competency.Prerequisite; +import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; + +public interface PrerequisiteRepository extends JpaRepository { + + // TODO: needed -> if yes we need to add linkedCompetency to prerequisite -> Would it make sense to also add to competency? + /* + * @Query(""" + * SELECT c + * FROM Competency c + * LEFT JOIN FETCH c.consecutiveCourses + * WHERE c.id = :competencyId + * """) + * Optional findByIdWithConsecutiveCourses(@Param("competencyId") long competencyId); + */ + + List findByCourseIdOrderByTitle(long courseId); + + Long countByCourseId(long courseId); + + Optional findByIdAndCourseId(long prerequisiteId, long courseId); + + default Prerequisite findByIdAndCourseIdElseThrow(long prerequisiteId, long courseId) throws EntityNotFoundException { + return findByIdAndCourseId(prerequisiteId, courseId).orElseThrow(() -> new EntityNotFoundException("Prerequisite", prerequisiteId)); + } + + boolean existsByIdAndCourseId(long prerequisiteId, long courseId); + + default void existsByIdAndCourseIdElseThrow(long prerequisiteId, long courseId) throws EntityNotFoundException { + if (!existsByIdAndCourseId(prerequisiteId, courseId)) { + throw new EntityNotFoundException("Prerequisite", prerequisiteId); + } + } +} 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 new file mode 100644 index 000000000000..3cedf22344ec --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/competency/PrerequisiteService.java @@ -0,0 +1,76 @@ +package de.tum.in.www1.artemis.service.competency; + +import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import de.tum.in.www1.artemis.domain.competency.Prerequisite; +import de.tum.in.www1.artemis.repository.CourseRepository; +import de.tum.in.www1.artemis.repository.PrerequisiteRepository; +import de.tum.in.www1.artemis.web.rest.dto.competency.PrerequisiteRequestDTO; + +/** + * Service for managing prerequisite competencies. + */ +@Profile(PROFILE_CORE) +@Service +public class PrerequisiteService { + + private final PrerequisiteRepository prerequisiteRepository; + + private final CourseRepository courseRepository; + + public PrerequisiteService(PrerequisiteRepository prerequisiteRepository, CourseRepository courseRepository) { + this.prerequisiteRepository = prerequisiteRepository; + this.courseRepository = courseRepository; + } + + /** + * Creates a new prerequisite with the given values in the given course + * + * @param prerequisiteValues the values of the prerequisite to create + * @param courseId the id of the course to create the prerequisite in + * @return the created prerequisite + */ + public Prerequisite createPrerequisite(PrerequisiteRequestDTO prerequisiteValues, long courseId) { + var course = courseRepository.findByIdElseThrow(courseId); + + var prerequisiteToCreate = new Prerequisite(prerequisiteValues.title(), prerequisiteValues.description(), prerequisiteValues.softDueDate(), + prerequisiteValues.masteryThreshold(), prerequisiteValues.taxonomy(), prerequisiteValues.optional()); + prerequisiteToCreate.setCourse(course); + + return prerequisiteRepository.save(prerequisiteToCreate); + } + + /** + * Updates an existing prerequisite with the given values if it is part of the given course + * + * @param prerequisiteValues the new prerequisite values + * @param prerequisiteId the id of the prerequisite to update + * @param courseId the id of the course the prerequisite is part of + * @return the updated prerequisite + */ + public Prerequisite updatePrerequisite(PrerequisiteRequestDTO prerequisiteValues, long prerequisiteId, long courseId) { + var existingPrerequisite = prerequisiteRepository.findByIdAndCourseIdElseThrow(prerequisiteId, courseId); + + existingPrerequisite.setTitle(prerequisiteValues.title()); + existingPrerequisite.setDescription(prerequisiteValues.description()); + existingPrerequisite.setTaxonomy(prerequisiteValues.taxonomy()); + existingPrerequisite.setMasteryThreshold(prerequisiteValues.masteryThreshold()); + existingPrerequisite.setOptional(prerequisiteValues.optional()); + + 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); + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteRequestDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteRequestDTO.java new file mode 100644 index 000000000000..b18954838d27 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteRequestDTO.java @@ -0,0 +1,20 @@ +package de.tum.in.www1.artemis.web.rest.dto.competency; + +import java.time.ZonedDateTime; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.in.www1.artemis.domain.competency.AbstractCompetency; +import de.tum.in.www1.artemis.domain.competency.CompetencyTaxonomy; +import de.tum.in.www1.artemis.domain.competency.Prerequisite; + +/** + * DTO used to send create/update requests regarding {@link Prerequisite} objects. + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record PrerequisiteRequestDTO(@NotBlank @Size(min = 1, max = AbstractCompetency.MAX_TITLE_LENGTH) String title, String description, CompetencyTaxonomy taxonomy, + ZonedDateTime softDueDate, int masteryThreshold, boolean optional) { +} diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteResponseDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteResponseDTO.java new file mode 100644 index 000000000000..2e6f86e2047f --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteResponseDTO.java @@ -0,0 +1,20 @@ +package de.tum.in.www1.artemis.web.rest.dto.competency; + +import java.time.ZonedDateTime; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.in.www1.artemis.domain.competency.CompetencyTaxonomy; +import de.tum.in.www1.artemis.domain.competency.Prerequisite; + +/** + * DTO used to send responses regarding {@link Prerequisite} objects. + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record PrerequisiteResponseDTO(long id, String title, String description, CompetencyTaxonomy taxonomy, ZonedDateTime softDueDate, int masteryThreshold, boolean optional) { + + public static PrerequisiteResponseDTO of(Prerequisite prerequisite) { + return new PrerequisiteResponseDTO(prerequisite.getId(), prerequisite.getTitle(), prerequisite.getDescription(), prerequisite.getTaxonomy(), prerequisite.getSoftDueDate(), + prerequisite.getMasteryThreshold(), prerequisite.isOptional()); + } +} From a69a1afe6001abf0f35fe8c28df6838a7e647104 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Fri, 24 May 2024 15:32:55 +0200 Subject: [PATCH 05/78] Add Prerequisite Endpoints, Change Authorization to @EnforceAtLeastRoleInCourse --- .../domain/competency/AbstractCompetency.java | 3 + .../artemis/web/rest/CompetencyResource.java | 182 ++++++++++-------- 2 files changed, 110 insertions(+), 75 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/domain/competency/AbstractCompetency.java b/src/main/java/de/tum/in/www1/artemis/domain/competency/AbstractCompetency.java index fcf8077abebc..3fb151a84d8a 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/competency/AbstractCompetency.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/competency/AbstractCompetency.java @@ -36,6 +36,9 @@ public abstract class AbstractCompetency extends BaseCompetency { @JsonIgnore public static final int DEFAULT_MASTERY_THRESHOLD = 50; + @JsonIgnore + public static final int MAX_TITLE_LENGTH = 255; + @Column(name = "soft_due_date") private ZonedDateTime softDueDate; diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java index 2a5965610bb8..4146481d06f5 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java @@ -4,7 +4,6 @@ import java.net.URI; import java.net.URISyntaxException; -import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Objects; @@ -41,18 +40,21 @@ import de.tum.in.www1.artemis.repository.CompetencyRelationRepository; import de.tum.in.www1.artemis.repository.CompetencyRepository; 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.security.Role; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastEditor; -import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent; import de.tum.in.www1.artemis.security.annotations.enforceRoleInCourse.EnforceAtLeastEditorInCourse; +import de.tum.in.www1.artemis.security.annotations.enforceRoleInCourse.EnforceAtLeastInstructorInCourse; +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.ExerciseService; import de.tum.in.www1.artemis.service.LectureUnitService; 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.PrerequisiteService; import de.tum.in.www1.artemis.service.iris.session.IrisCompetencyGenerationSessionService; import de.tum.in.www1.artemis.service.util.TimeLogUtil; import de.tum.in.www1.artemis.web.rest.dto.CourseCompetencyProgressDTO; @@ -60,6 +62,8 @@ import de.tum.in.www1.artemis.web.rest.dto.competency.CompetencyImportResponseDTO; import de.tum.in.www1.artemis.web.rest.dto.competency.CompetencyRelationDTO; import de.tum.in.www1.artemis.web.rest.dto.competency.CompetencyWithTailRelationDTO; +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.dto.pageablesearch.CompetencyPageableSearchDTO; import de.tum.in.www1.artemis.web.rest.dto.pageablesearch.SearchTermPageableSearchDTO; import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; @@ -103,11 +107,16 @@ public class CompetencyResource { private final CompetencyRelationService competencyRelationService; + private final PrerequisiteService prerequisiteService; + + private final PrerequisiteRepository prerequisiteRepository; + 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) { + Optional irisCompetencyGenerationSessionService, PrerequisiteService prerequisiteService, + PrerequisiteRepository prerequisiteRepository) { this.courseRepository = courseRepository; this.competencyRelationRepository = competencyRelationRepository; this.authorizationCheckService = authorizationCheckService; @@ -120,6 +129,8 @@ public CompetencyResource(CourseRepository courseRepository, AuthorizationCheckS this.lectureUnitService = lectureUnitService; this.competencyRelationService = competencyRelationService; this.irisCompetencyGenerationSessionService = irisCompetencyGenerationSessionService; + this.prerequisiteService = prerequisiteService; + this.prerequisiteRepository = prerequisiteRepository; } /** @@ -169,12 +180,10 @@ public ResponseEntity> getCompetenciesForImport( * @return the ResponseEntity with status 200 (OK) and with body the found competencies */ @GetMapping("courses/{courseId}/competencies") - @EnforceAtLeastStudent + @EnforceAtLeastStudentInCourse public ResponseEntity> getCompetenciesWithProgress(@PathVariable long courseId) { log.debug("REST request to get competencies for course with id: {}", courseId); - Course course = courseRepository.findByIdElseThrow(courseId); User user = userRepository.getUserWithGroupsAndAuthorities(); - authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, user); final var competencies = competencyService.findCompetenciesWithProgressForUserByCourseId(courseId, user.getId()); return ResponseEntity.ok(competencies); } @@ -188,7 +197,7 @@ public ResponseEntity> getCompetenciesWithProgress(@PathVariabl * @return the ResponseEntity with status 200 (OK) and with body the competency, or with status 404 (Not Found) */ @GetMapping("courses/{courseId}/competencies/{competencyId}") - @EnforceAtLeastStudent + @EnforceAtLeastStudentInCourse public ResponseEntity getCompetency(@PathVariable long competencyId, @PathVariable long courseId) { log.info("REST request to get Competency : {}", competencyId); long start = System.nanoTime(); @@ -217,7 +226,7 @@ public ResponseEntity getCompetency(@PathVariable long competencyId, * @return the ResponseEntity with status 200 (OK) and with body the updated competency */ @PutMapping("courses/{courseId}/competencies") - @EnforceAtLeastInstructor + @EnforceAtLeastInstructorInCourse public ResponseEntity updateCompetency(@PathVariable long courseId, @RequestBody Competency competency) { log.debug("REST request to update Competency : {}", competency); if (competency.getId() == null) { @@ -242,14 +251,13 @@ public ResponseEntity updateCompetency(@PathVariable long courseId, * @throws URISyntaxException if the Location URI syntax is incorrect */ @PostMapping("courses/{courseId}/competencies") - @EnforceAtLeastInstructor + @EnforceAtLeastInstructorInCourse public ResponseEntity createCompetency(@PathVariable long courseId, @RequestBody Competency competency) throws URISyntaxException { log.debug("REST request to create Competency : {}", competency); if (competency.getId() != null || competency.getTitle() == null || competency.getTitle().trim().isEmpty()) { throw new BadRequestException(); } var course = courseRepository.findWithEagerCompetenciesByIdElseThrow(courseId); - authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null); final var persistedCompetency = competencyService.createCompetency(competency, course); @@ -265,7 +273,7 @@ public ResponseEntity createCompetency(@PathVariable long courseId, * @throws URISyntaxException if the Location URI syntax is incorrect */ @PostMapping("courses/{courseId}/competencies/bulk") - @EnforceAtLeastInstructor + @EnforceAtLeastInstructorInCourse public ResponseEntity> createCompetencies(@PathVariable Long courseId, @RequestBody List competencies) throws URISyntaxException { log.debug("REST request to create Competencies : {}", competencies); for (Competency competency : competencies) { @@ -274,7 +282,6 @@ public ResponseEntity> createCompetencies(@PathVariable Long co } } var course = courseRepository.findWithEagerCompetenciesByIdElseThrow(courseId); - authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null); var createdCompetencies = competencyService.createCompetencies(competencies, course); @@ -290,12 +297,11 @@ public ResponseEntity> createCompetencies(@PathVariable Long co * @throws URISyntaxException if the Location URI syntax is incorrect */ @PostMapping("courses/{courseId}/competencies/import") - @EnforceAtLeastInstructor + @EnforceAtLeastInstructorInCourse 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); - authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null); authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, competencyToImport.getCourse(), null); if (competencyToImport.getCourse().getId().equals(courseId)) { @@ -317,13 +323,12 @@ public ResponseEntity importCompetency(@PathVariable long courseId, * @throws URISyntaxException if the Location URI syntax is incorrect */ @PostMapping("courses/{courseId}/competencies/import/bulk") - @EnforceAtLeastEditor + @EnforceAtLeastEditorInCourse public ResponseEntity> importCompetencies(@PathVariable long courseId, @RequestBody List competenciesToImport, @RequestParam(defaultValue = "false") boolean importRelations) throws URISyntaxException { log.info("REST request to import competencies: {}", competenciesToImport); var course = courseRepository.findWithEagerCompetenciesByIdElseThrow(courseId); - authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, course, null); var competencies = new HashSet<>(competenciesToImport); List importedCompetencies; @@ -350,7 +355,7 @@ public ResponseEntity> importCompetencies(@P * @throws URISyntaxException if the Location URI syntax is incorrect */ @PostMapping("courses/{courseId}/competencies/import-all/{sourceCourseId}") - @EnforceAtLeastInstructor + @EnforceAtLeastInstructorInCourse public ResponseEntity> importAllCompetenciesFromCourse(@PathVariable long courseId, @PathVariable long sourceCourseId, @RequestParam(defaultValue = "false") boolean importRelations) throws URISyntaxException { log.info("REST request to all competencies from course {} into course {}", sourceCourseId, courseId); @@ -360,7 +365,6 @@ public ResponseEntity> importAllCompetencies } var targetCourse = courseRepository.findByIdElseThrow(courseId); var sourceCourse = courseRepository.findByIdElseThrow(sourceCourseId); - authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, targetCourse, null); authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, sourceCourse, null); var competencies = competencyRepository.findAllForCourse(sourceCourse.getId()); @@ -404,7 +408,7 @@ public ResponseEntity> importStandardizedCompe * @return the ResponseEntity with status 200 (OK) */ @DeleteMapping("courses/{courseId}/competencies/{competencyId}") - @EnforceAtLeastInstructor + @EnforceAtLeastInstructorInCourse public ResponseEntity deleteCompetency(@PathVariable long competencyId, @PathVariable long courseId) { log.info("REST request to delete a Competency : {}", competencyId); @@ -426,7 +430,7 @@ public ResponseEntity deleteCompetency(@PathVariable long competencyId, @P * @return the ResponseEntity with status 200 (OK) and with the competency course performance in the body */ @GetMapping("courses/{courseId}/competencies/{competencyId}/student-progress") - @EnforceAtLeastStudent + @EnforceAtLeastStudentInCourse public ResponseEntity getCompetencyStudentProgress(@PathVariable long courseId, @PathVariable long competencyId, @RequestParam(defaultValue = "false") Boolean refresh) { log.debug("REST request to get student progress for competency: {}", competencyId); @@ -454,7 +458,7 @@ public ResponseEntity getCompetencyStudentProgress(@PathVari * @return the ResponseEntity with status 200 (OK) and with the competency course performance in the body */ @GetMapping("courses/{courseId}/competencies/{competencyId}/course-progress") - @EnforceAtLeastInstructor + @EnforceAtLeastInstructorInCourse public ResponseEntity getCompetencyCourseProgress(@PathVariable long courseId, @PathVariable long competencyId) { log.debug("REST request to get course progress for competency: {}", competencyId); var course = courseRepository.findByIdElseThrow(courseId); @@ -466,6 +470,8 @@ public ResponseEntity getCompetencyCourseProgress(@ return ResponseEntity.ok().body(progress); } + // Competency Relation Endpoints + /** * GET courses/:courseId/competencies/relations get the relations for the course * @@ -473,11 +479,9 @@ public ResponseEntity getCompetencyCourseProgress(@ * @return the ResponseEntity with status 200 (OK) and with a list of relations for the course */ @GetMapping("courses/{courseId}/competencies/relations") - @EnforceAtLeastInstructor + @EnforceAtLeastInstructorInCourse public ResponseEntity> getCompetencyRelations(@PathVariable long courseId) { log.debug("REST request to get relations for course: {}", courseId); - var course = courseRepository.findByIdElseThrow(courseId); - authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null); var relations = competencyRelationRepository.findAllWithHeadAndTailByCourseId(courseId); var relationDTOs = relations.stream().map(CompetencyRelationDTO::of).collect(Collectors.toSet()); @@ -493,7 +497,7 @@ public ResponseEntity> getCompetencyRelations(@PathVa * @return the ResponseEntity with status 200 (OK) */ @PostMapping("courses/{courseId}/competencies/relations") - @EnforceAtLeastInstructor + @EnforceAtLeastInstructorInCourse public ResponseEntity createCompetencyRelation(@PathVariable long courseId, @RequestBody CompetencyRelation relation) { var tailId = relation.getTailCompetency().getId(); var headId = relation.getHeadCompetency().getId(); @@ -518,7 +522,7 @@ public ResponseEntity createCompetencyRelation(@PathVariable * @return the ResponseEntity with status 200 (OK) */ @DeleteMapping("courses/{courseId}/competencies/relations/{competencyRelationId}") - @EnforceAtLeastInstructor + @EnforceAtLeastInstructorInCourse public ResponseEntity removeCompetencyRelation(@PathVariable long courseId, @PathVariable long competencyRelationId) { log.info("REST request to remove a competency relation: {}", competencyRelationId); var course = courseRepository.findByIdElseThrow(courseId); @@ -532,6 +536,8 @@ public ResponseEntity removeCompetencyRelation(@PathVariable long courseId return ResponseEntity.ok().build(); } + // Prerequisite Endpoints + /** * GET /courses/:courseId/prerequisites * @@ -540,76 +546,103 @@ public ResponseEntity removeCompetencyRelation(@PathVariable long courseId */ @GetMapping("courses/{courseId}/prerequisites") @EnforceAtLeastStudent - public ResponseEntity> getPrerequisites(@PathVariable long courseId) { + public ResponseEntity> getPrerequisites(@PathVariable long courseId) { log.debug("REST request to get prerequisites for course with id: {}", courseId); Course course = courseRepository.findByIdElseThrow(courseId); - User user = userRepository.getUserWithGroupsAndAuthorities(); - // Authorization check is skipped when course is open to self-enrollment + // Allow any student to see the prerequisites if the course is open to self-enrollment if (!course.isEnrollmentEnabled()) { - authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, user); + authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, null); } - Set prerequisites = competencyService.findAllPrerequisitesForCourse(course); + var prerequisites = prerequisiteRepository.findByCourseIdOrderByTitle(courseId); - return ResponseEntity.ok(new ArrayList<>(prerequisites)); + return ResponseEntity.ok(prerequisites.stream().map(PrerequisiteResponseDTO::of).toList()); } /** - * POST /courses/:courseId/prerequisites/:competencyId + * POST /courses/:courseId/prerequisites : creates a new prerequisite competency. * - * @param courseId the id of the course for which the competency should be a prerequisite - * @param competencyId the id of the prerequisite (competency) to add + * @param courseId the id of the course to which the competency should be added + * @param prerequisite the prerequisite that should be created + * @return the ResponseEntity with status 201 (Created) and with body the new prerequisite + * @throws URISyntaxException if the Location URI syntax is incorrect + */ + @PostMapping("courses/{courseId}/prerequisites") + @EnforceAtLeastEditorInCourse + public ResponseEntity createPrerequisite(@PathVariable long courseId, @RequestBody PrerequisiteRequestDTO prerequisite) throws URISyntaxException { + log.debug("REST request to create Prerequisite : {}", prerequisite); + + final var savedPrerequisite = prerequisiteService.createPrerequisite(prerequisite, courseId); + final var uri = new URI("/api/courses/" + courseId + "/prerequisites/" + savedPrerequisite.getId()); + + return ResponseEntity.created(uri).body(PrerequisiteResponseDTO.of(savedPrerequisite)); + } + + /** + * PUT /courses/:courseId/prerequisites/:prerequisiteId : updates an existing prerequisite + * + * @param courseId the id of the course to which the prerequisite belongs + * @param prerequisiteId the id of the prerequisite to update + * @param prerequisiteValues the new prerequisite values * @return the ResponseEntity with status 200 (OK) */ - @PostMapping("courses/{courseId}/prerequisites/{competencyId}") - @EnforceAtLeastInstructor - public ResponseEntity addPrerequisite(@PathVariable long competencyId, @PathVariable long courseId) { - log.info("REST request to add a prerequisite: {}", competencyId); - var course = courseRepository.findWithEagerCompetenciesByIdElseThrow(courseId); - // var competency = competencyRepository.findByIdWithConsecutiveCoursesElseThrow(competencyId); - var competency = new Competency(); - authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null); - authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, competency.getCourse(), null); + @PutMapping("courses/{courseId}/prerequisites/{prerequisiteId}") + @EnforceAtLeastEditorInCourse + public ResponseEntity updatePrerequisite(@PathVariable long courseId, @PathVariable long prerequisiteId, + @RequestBody PrerequisiteRequestDTO prerequisiteValues) throws URISyntaxException { + log.info("REST request to update Prerequisite with id : {}", prerequisiteId); - if (competency.getCourse().getId().equals(courseId)) { - throw new BadRequestAlertException("The competency of a course can not be a prerequisite to the same course", ENTITY_NAME, "competencyCycle"); - } + final var savedPrerequisite = prerequisiteService.updatePrerequisite(prerequisiteValues, prerequisiteId, courseId); - // TODO: do notuse competencyId. - // TODO: add new logic. - return ResponseEntity.ok().body(competency); + return ResponseEntity.ok(PrerequisiteResponseDTO.of(savedPrerequisite)); } /** - * DELETE /courses/:courseId/prerequisites/:competencyId + * DELETE /courses/:courseId/prerequisites/:prerequisiteId : deletes an existing prerequisite * - * @param courseId the id of the course for which the competency is a prerequisite - * @param competencyId the id of the prerequisite (competency) to remove + * @param courseId the id of the course to which the prerequisite belongs + * @param prerequisiteId the id of the prerequisite to remove * @return the ResponseEntity with status 200 (OK) */ - @DeleteMapping("courses/{courseId}/prerequisites/{competencyId}") - @EnforceAtLeastInstructor - public ResponseEntity removePrerequisite(@PathVariable long competencyId, @PathVariable long courseId) { - log.info("REST request to remove a prerequisite: {}", competencyId); - var course = courseRepository.findWithEagerCompetenciesByIdElseThrow(courseId); - // var competency = competencyRepository.findByIdWithConsecutiveCoursesElseThrow(competencyId); - var competency = new Competency(); - // TODO: replace logic. - /* - * if (!competency.getConsecutiveCourses().stream().map(Course::getId).toList().contains(courseId)) { - * throw new BadRequestAlertException("The competency is not a prerequisite of the given course", ENTITY_NAME, "prerequisiteWrongCourse"); - * } - */ - authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null); - authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, competency.getCourse(), null); - - // TODO: add new logic. - courseRepository.save(course); - - return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, PREREQUISITE_NAME, competency.getTitle())).build(); + @DeleteMapping("courses/{courseId}/prerequisites/{prerequisiteId}") + @EnforceAtLeastEditorInCourse + public ResponseEntity deletePrerequisite(@PathVariable long prerequisiteId, @PathVariable long courseId) { + log.info("REST request to delete Prerequisite with id : {}", prerequisiteId); + + prerequisiteService.deletePrerequisite(prerequisiteId, courseId); + + // TODO: add notification on the client-side. + return ResponseEntity.ok().build(); } + // TODO: re-add prerequisite import. + /** + * POST /courses/:courseId/prerequisites/:competencyId + * + * @param courseId the id of the course for which the competency should be a prerequisite + * @param competencyId the id of the prerequisite (competency) to add + * @return the ResponseEntity with status 200 (OK) + */ + /* + * @PostMapping("courses/{courseId}/prerequisites/{competencyId}") + * @EnforceAtLeastInstructorInCourse + * public ResponseEntity addPrerequisite(@PathVariable long competencyId, @PathVariable long courseId) { + * log.info("REST request to add a prerequisite: {}", competencyId); + * var course = courseRepository.findWithEagerCompetenciesByIdElseThrow(courseId); + * // var competency = competencyRepository.findByIdWithConsecutiveCoursesElseThrow(competencyId); + * var competency = new Competency(); + * authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null); + * authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, competency.getCourse(), null); + * if (competency.getCourse().getId().equals(courseId)) { + * throw new BadRequestAlertException("The competency of a course can not be a prerequisite to the same course", ENTITY_NAME, "competencyCycle"); + * } + * // TODO: do notuse competencyId. + * // TODO: add new logic. + * return ResponseEntity.ok().body(competency); + * } + */ + /** * Generates a list of competencies from a given course description by using IRIS. * @@ -618,12 +651,11 @@ public ResponseEntity removePrerequisite(@PathVariable long competencyId, * @return the ResponseEntity with status 200 (OK) and body the genrated competencies */ @PostMapping("courses/{courseId}/competencies/generate-from-description") - @EnforceAtLeastEditor + @EnforceAtLeastEditorInCourse public ResponseEntity> generateCompetenciesFromCourseDescription(@PathVariable Long courseId, @RequestBody String courseDescription) { var irisService = irisCompetencyGenerationSessionService.orElseThrow(); var user = userRepository.getUserWithGroupsAndAuthorities(); var course = courseRepository.findByIdElseThrow(courseId); - authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, course, user); var session = irisService.getOrCreateSession(course, user); irisService.addUserTextMessageToSession(session, courseDescription); From ab982acde8694669e37e8bff4eed1ec0e781f5e8 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Sun, 26 May 2024 14:18:15 +0200 Subject: [PATCH 06/78] Move prerequisites to own resource, add prerequisite import endpoint --- .../de/tum/in/www1/artemis/domain/Course.java | 1 - .../artemis/domain/competency/Competency.java | 6 +- ...tCompetency.java => CourseCompetency.java} | 28 +++- .../domain/competency/Prerequisite.java | 3 +- .../CourseCompetencyRepository.java | 37 +++++ .../competency/PrerequisiteService.java | 58 ++++++- .../artemis/web/rest/CompetencyResource.java | 117 ------------- .../rest/competency/PrerequisiteResource.java | 155 ++++++++++++++++++ .../competency/PrerequisiteRequestDTO.java | 4 +- .../competency/PrerequisiteResponseDTO.java | 15 +- .../changelog/20240523180900_changelog.xml | 18 +- .../competency/CompetencyUtilService.java | 19 +++ .../artemis/course/CourseTestService.java | 5 +- .../lecture/CompetencyIntegrationTest.java | 122 +++++++------- 14 files changed, 385 insertions(+), 203 deletions(-) rename src/main/java/de/tum/in/www1/artemis/domain/competency/{AbstractCompetency.java => CourseCompetency.java} (80%) create mode 100644 src/main/java/de/tum/in/www1/artemis/repository/CourseCompetencyRepository.java create mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/competency/PrerequisiteResource.java diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Course.java b/src/main/java/de/tum/in/www1/artemis/domain/Course.java index fe36970a89ca..8365120c5a73 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Course.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Course.java @@ -246,7 +246,6 @@ public class Course extends DomainObject { @JsonIgnoreProperties("course") private Set organizations = new HashSet<>(); - @ManyToMany @OneToMany(mappedBy = "course", cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.LAZY) @JsonIgnoreProperties("course") @OrderBy("title") 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 b1f8373725b1..43abba8ed0e9 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 @@ -18,14 +18,14 @@ @Entity @DiscriminatorValue("COMPETENCY") -public class Competency extends AbstractCompetency { +public class Competency extends CourseCompetency { - // TODO: move to AbstractCompetency + // TODO: move to CourseCompetency in next step of refactoring @ManyToMany(mappedBy = "competencies") @JsonIgnoreProperties({ "competencies", "course" }) private Set exercises = new HashSet<>(); - // TODO: move to AbstractCompetency + // TODO: move to CourseCompetency in next step of refactoring @ManyToMany(mappedBy = "competencies") @JsonIgnoreProperties("competencies") private Set lectureUnits = new HashSet<>(); diff --git a/src/main/java/de/tum/in/www1/artemis/domain/competency/AbstractCompetency.java b/src/main/java/de/tum/in/www1/artemis/domain/competency/CourseCompetency.java similarity index 80% rename from src/main/java/de/tum/in/www1/artemis/domain/competency/AbstractCompetency.java rename to src/main/java/de/tum/in/www1/artemis/domain/competency/CourseCompetency.java index 3fb151a84d8a..2010741b3922 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/competency/AbstractCompetency.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/competency/CourseCompetency.java @@ -26,15 +26,16 @@ import de.tum.in.www1.artemis.domain.Course; +// TODO: javadoc for this. @Entity @Table(name = "competency") @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "discriminator", discriminatorType = DiscriminatorType.STRING) @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) -public abstract class AbstractCompetency extends BaseCompetency { +public abstract class CourseCompetency extends BaseCompetency { @JsonIgnore - public static final int DEFAULT_MASTERY_THRESHOLD = 50; + public static final int DEFAULT_MASTERY_THRESHOLD = 100; @JsonIgnore public static final int MAX_TITLE_LENGTH = 255; @@ -43,7 +44,7 @@ public abstract class AbstractCompetency extends BaseCompetency { private ZonedDateTime softDueDate; @Column(name = "mastery_threshold") - private Integer masteryThreshold; + private int masteryThreshold; @Column(name = "optional") private boolean optional; @@ -58,6 +59,11 @@ public abstract class AbstractCompetency extends BaseCompetency { @JsonIgnoreProperties({ "competencies" }) private StandardizedCompetency linkedStandardizedCompetency; + @ManyToOne + @JoinColumn(name = "linked_course_competency_id") + @JsonIgnoreProperties({ "competencies" }) + private CourseCompetency linkedCourseCompetency; + @OneToMany(mappedBy = "competency", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE, orphanRemoval = true) @JsonIgnoreProperties({ "user", "competency" }) private Set userProgress = new HashSet<>(); @@ -66,10 +72,10 @@ public abstract class AbstractCompetency extends BaseCompetency { @JsonIgnoreProperties({ "competencies", "course" }) private Set learningPaths = new HashSet<>(); - public AbstractCompetency() { + public CourseCompetency() { } - public AbstractCompetency(String title, String description, ZonedDateTime softDueDate, Integer masteryThreshold, CompetencyTaxonomy taxonomy, boolean optional) { + public CourseCompetency(String title, String description, ZonedDateTime softDueDate, Integer masteryThreshold, CompetencyTaxonomy taxonomy, boolean optional) { super(title, description, taxonomy); this.softDueDate = softDueDate; this.masteryThreshold = masteryThreshold; @@ -84,11 +90,11 @@ public void setSoftDueDate(ZonedDateTime softDueDate) { this.softDueDate = softDueDate; } - public Integer getMasteryThreshold() { + public int getMasteryThreshold() { return masteryThreshold; } - public void setMasteryThreshold(Integer masteryThreshold) { + public void setMasteryThreshold(int masteryThreshold) { this.masteryThreshold = masteryThreshold; } @@ -116,6 +122,14 @@ public void setLinkedStandardizedCompetency(StandardizedCompetency linkedStandar this.linkedStandardizedCompetency = linkedStandardizedCompetency; } + public CourseCompetency getLinkedCourseCompetency() { + return linkedCourseCompetency; + } + + public void setLinkedCourseCompetency(CourseCompetency linkedCourseCompetency) { + this.linkedCourseCompetency = linkedCourseCompetency; + } + public Set getUserProgress() { return userProgress; } 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 8d6072baf53d..325f9d60de76 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 @@ -5,9 +5,10 @@ import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; +// TODO: javadco @Entity @DiscriminatorValue("PREREQUISITE") -public class Prerequisite extends AbstractCompetency { +public class Prerequisite extends CourseCompetency { public Prerequisite(String title, String description, ZonedDateTime softDueDate, Integer masteryThreshold, CompetencyTaxonomy taxonomy, boolean optional) { super(title, description, softDueDate, masteryThreshold, taxonomy, optional); 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 new file mode 100644 index 000000000000..2e973ed97707 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/repository/CourseCompetencyRepository.java @@ -0,0 +1,37 @@ +package de.tum.in.www1.artemis.repository; + +import java.util.List; +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.competency.CourseCompetency; +import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; + +public interface CourseCompetencyRepository extends JpaRepository { + + @Query(""" + SELECT c + FROM CourseCompetency c + WHERE c.id IN :courseCompetencyIds AND (c.course.instructorGroupName IN :groups OR c.course.editorGroupName IN :groups) + """) + List findAllByIdAndUserIsAtLeastEditorInCourse(@Param("courseCompetencyIds") List courseCompetencyIds, @Param("groups") Set groups); + + default List findAllByIdAndUserIsAtLeastEditorInCourseElseThrow(List courseCompetencyIds, Set userGroups) { + var courseCompetencies = findAllByIdAndUserIsAtLeastEditorInCourse(courseCompetencyIds, userGroups); + if (courseCompetencies.size() != courseCompetencyIds.size()) { + throw new EntityNotFoundException("Could not find all requested courseCompetencies!"); + } + return courseCompetencies; + } + + default List findAllByIdElseThrow(List courseCompetencyIds) { + var courseCompetencies = findAllById(courseCompetencyIds); + if (courseCompetencies.size() != courseCompetencyIds.size()) { + throw new EntityNotFoundException("Could not find all requested courseCompetencies!"); + } + return courseCompetencies; + } +} 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 3cedf22344ec..9b9df9b58551 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,12 +2,19 @@ import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; +import java.util.ArrayList; +import java.util.List; + 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.domain.competency.Prerequisite; +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.web.rest.dto.competency.PrerequisiteRequestDTO; /** @@ -21,9 +28,19 @@ public class PrerequisiteService { private final CourseRepository courseRepository; - public PrerequisiteService(PrerequisiteRepository prerequisiteRepository, CourseRepository courseRepository) { + private final CourseCompetencyRepository courseCompetencyRepository; + + private final UserRepository userRepository; + + private final AuthorizationCheckService authorizationCheckService; + + public PrerequisiteService(PrerequisiteRepository prerequisiteRepository, CourseRepository courseRepository, CourseCompetencyRepository courseCompetencyRepository, + UserRepository userRepository, AuthorizationCheckService authorizationCheckService) { this.prerequisiteRepository = prerequisiteRepository; this.courseRepository = courseRepository; + this.courseCompetencyRepository = courseCompetencyRepository; + this.userRepository = userRepository; + this.authorizationCheckService = authorizationCheckService; } /** @@ -73,4 +90,43 @@ 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 + * + * @param courseId the course to import into + * @param courseCompetencyIds the ids of the courseCompetencies to import + * @return The list of imported prerequisites + */ + public List importPrerequisites(long courseId, List courseCompetencyIds) { + var course = courseRepository.findByIdElseThrow(courseId); + var user = userRepository.getUser(); + List courseCompetenciesToImport; + if (authorizationCheckService.isAdmin(user)) { + courseCompetenciesToImport = courseCompetencyRepository.findAllByIdElseThrow(courseCompetencyIds); + } + else { + courseCompetenciesToImport = courseCompetencyRepository.findAllByIdAndUserIsAtLeastEditorInCourseElseThrow(courseCompetencyIds, user.getGroups()); + } + + var prerequisitesToImport = new ArrayList(); + for (var competency : courseCompetenciesToImport) { + var prerequisiteToImport = new Prerequisite(); + prerequisiteToImport.setTitle(competency.getTitle()); + prerequisiteToImport.setDescription(competency.getDescription()); + prerequisiteToImport.setTaxonomy(competency.getTaxonomy()); + prerequisiteToImport.setOptional(competency.isOptional()); + prerequisiteToImport.setMasteryThreshold(competency.getMasteryThreshold()); + // do not set due date + prerequisiteToImport.setLinkedCourseCompetency(competency); + prerequisiteToImport.setCourse(course); + + prerequisitesToImport.add(prerequisiteToImport); + } + + // TODO: link to learning paths once we support them for prerequisites. + // TODO: import relations once we support them for prerequisites. + + return prerequisiteRepository.saveAll(prerequisitesToImport); + } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java index 4146481d06f5..f03b154e48ee 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java @@ -62,8 +62,6 @@ import de.tum.in.www1.artemis.web.rest.dto.competency.CompetencyImportResponseDTO; import de.tum.in.www1.artemis.web.rest.dto.competency.CompetencyRelationDTO; import de.tum.in.www1.artemis.web.rest.dto.competency.CompetencyWithTailRelationDTO; -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.dto.pageablesearch.CompetencyPageableSearchDTO; import de.tum.in.www1.artemis.web.rest.dto.pageablesearch.SearchTermPageableSearchDTO; import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; @@ -81,8 +79,6 @@ public class CompetencyResource { private static final String ENTITY_NAME = "competency"; - private static final String PREREQUISITE_NAME = "prerequisite"; - private final CourseRepository courseRepository; private final AuthorizationCheckService authorizationCheckService; @@ -107,10 +103,6 @@ public class CompetencyResource { private final CompetencyRelationService competencyRelationService; - private final PrerequisiteService prerequisiteService; - - private final PrerequisiteRepository prerequisiteRepository; - public CompetencyResource(CourseRepository courseRepository, AuthorizationCheckService authorizationCheckService, UserRepository userRepository, CompetencyRepository competencyRepository, CompetencyRelationRepository competencyRelationRepository, CompetencyService competencyService, CompetencyProgressRepository competencyProgressRepository, CompetencyProgressService competencyProgressService, ExerciseService exerciseService, @@ -129,8 +121,6 @@ public CompetencyResource(CourseRepository courseRepository, AuthorizationCheckS this.lectureUnitService = lectureUnitService; this.competencyRelationService = competencyRelationService; this.irisCompetencyGenerationSessionService = irisCompetencyGenerationSessionService; - this.prerequisiteService = prerequisiteService; - this.prerequisiteRepository = prerequisiteRepository; } /** @@ -536,113 +526,6 @@ public ResponseEntity removeCompetencyRelation(@PathVariable long courseId return ResponseEntity.ok().build(); } - // Prerequisite Endpoints - - /** - * GET /courses/:courseId/prerequisites - * - * @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}/prerequisites") - @EnforceAtLeastStudent - public ResponseEntity> getPrerequisites(@PathVariable long courseId) { - log.debug("REST request to get prerequisites for course with id: {}", courseId); - Course course = courseRepository.findByIdElseThrow(courseId); - - // Allow any student to see the prerequisites if the course is open to self-enrollment - if (!course.isEnrollmentEnabled()) { - authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, null); - } - - var prerequisites = prerequisiteRepository.findByCourseIdOrderByTitle(courseId); - - return ResponseEntity.ok(prerequisites.stream().map(PrerequisiteResponseDTO::of).toList()); - } - - /** - * POST /courses/:courseId/prerequisites : creates a new prerequisite competency. - * - * @param courseId the id of the course to which the competency should be added - * @param prerequisite the prerequisite that should be created - * @return the ResponseEntity with status 201 (Created) and with body the new prerequisite - * @throws URISyntaxException if the Location URI syntax is incorrect - */ - @PostMapping("courses/{courseId}/prerequisites") - @EnforceAtLeastEditorInCourse - public ResponseEntity createPrerequisite(@PathVariable long courseId, @RequestBody PrerequisiteRequestDTO prerequisite) throws URISyntaxException { - log.debug("REST request to create Prerequisite : {}", prerequisite); - - final var savedPrerequisite = prerequisiteService.createPrerequisite(prerequisite, courseId); - final var uri = new URI("/api/courses/" + courseId + "/prerequisites/" + savedPrerequisite.getId()); - - return ResponseEntity.created(uri).body(PrerequisiteResponseDTO.of(savedPrerequisite)); - } - - /** - * PUT /courses/:courseId/prerequisites/:prerequisiteId : updates an existing prerequisite - * - * @param courseId the id of the course to which the prerequisite belongs - * @param prerequisiteId the id of the prerequisite to update - * @param prerequisiteValues the new prerequisite values - * @return the ResponseEntity with status 200 (OK) - */ - @PutMapping("courses/{courseId}/prerequisites/{prerequisiteId}") - @EnforceAtLeastEditorInCourse - public ResponseEntity updatePrerequisite(@PathVariable long courseId, @PathVariable long prerequisiteId, - @RequestBody PrerequisiteRequestDTO prerequisiteValues) throws URISyntaxException { - log.info("REST request to update Prerequisite with id : {}", prerequisiteId); - - final var savedPrerequisite = prerequisiteService.updatePrerequisite(prerequisiteValues, prerequisiteId, courseId); - - return ResponseEntity.ok(PrerequisiteResponseDTO.of(savedPrerequisite)); - } - - /** - * DELETE /courses/:courseId/prerequisites/:prerequisiteId : deletes an existing prerequisite - * - * @param courseId the id of the course to which the prerequisite belongs - * @param prerequisiteId the id of the prerequisite to remove - * @return the ResponseEntity with status 200 (OK) - */ - @DeleteMapping("courses/{courseId}/prerequisites/{prerequisiteId}") - @EnforceAtLeastEditorInCourse - public ResponseEntity deletePrerequisite(@PathVariable long prerequisiteId, @PathVariable long courseId) { - log.info("REST request to delete Prerequisite with id : {}", prerequisiteId); - - prerequisiteService.deletePrerequisite(prerequisiteId, courseId); - - // TODO: add notification on the client-side. - return ResponseEntity.ok().build(); - } - - // TODO: re-add prerequisite import. - /** - * POST /courses/:courseId/prerequisites/:competencyId - * - * @param courseId the id of the course for which the competency should be a prerequisite - * @param competencyId the id of the prerequisite (competency) to add - * @return the ResponseEntity with status 200 (OK) - */ - /* - * @PostMapping("courses/{courseId}/prerequisites/{competencyId}") - * @EnforceAtLeastInstructorInCourse - * public ResponseEntity addPrerequisite(@PathVariable long competencyId, @PathVariable long courseId) { - * log.info("REST request to add a prerequisite: {}", competencyId); - * var course = courseRepository.findWithEagerCompetenciesByIdElseThrow(courseId); - * // var competency = competencyRepository.findByIdWithConsecutiveCoursesElseThrow(competencyId); - * var competency = new Competency(); - * authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null); - * authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, competency.getCourse(), null); - * if (competency.getCourse().getId().equals(courseId)) { - * throw new BadRequestAlertException("The competency of a course can not be a prerequisite to the same course", ENTITY_NAME, "competencyCycle"); - * } - * // TODO: do notuse competencyId. - * // TODO: add new logic. - * return ResponseEntity.ok().body(competency); - * } - */ - /** * Generates a list of competencies from a given course description by using IRIS. * 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 new file mode 100644 index 000000000000..b75ee6d71b5a --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/competency/PrerequisiteResource.java @@ -0,0 +1,155 @@ +package de.tum.in.www1.artemis.web.rest.competency; + +import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.repository.CourseRepository; +import de.tum.in.www1.artemis.repository.PrerequisiteRepository; +import de.tum.in.www1.artemis.security.Role; +import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent; +import de.tum.in.www1.artemis.security.annotations.enforceRoleInCourse.EnforceAtLeastEditorInCourse; +import de.tum.in.www1.artemis.service.AuthorizationCheckService; +import de.tum.in.www1.artemis.service.competency.PrerequisiteService; +import de.tum.in.www1.artemis.web.rest.CompetencyResource; +import de.tum.in.www1.artemis.web.rest.dto.competency.PrerequisiteRequestDTO; +import de.tum.in.www1.artemis.web.rest.dto.competency.PrerequisiteResponseDTO; + +@Profile(PROFILE_CORE) +@RestController +@RequestMapping("api/") +public class PrerequisiteResource { + + private static final Logger log = LoggerFactory.getLogger(CompetencyResource.class); + + private static final String ENTITY_NAME = "prerequisite"; + + private final PrerequisiteService prerequisiteService; + + private final PrerequisiteRepository prerequisiteRepository; + + private final CourseRepository courseRepository; + + private final AuthorizationCheckService authorizationCheckService; + + public PrerequisiteResource(PrerequisiteService prerequisiteService, PrerequisiteRepository prerequisiteRepository, CourseRepository courseRepository, + AuthorizationCheckService authorizationCheckService) { + this.prerequisiteService = prerequisiteService; + this.prerequisiteRepository = prerequisiteRepository; + this.courseRepository = courseRepository; + this.authorizationCheckService = authorizationCheckService; + } + + /** + * GET /courses/:courseId/competencies/prerequisites + * + * @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}/competencies/prerequisites") + @EnforceAtLeastStudent + public ResponseEntity> getPrerequisites(@PathVariable long courseId) { + log.debug("REST request to get prerequisites for course with id: {}", courseId); + Course course = courseRepository.findByIdElseThrow(courseId); + + // Allow any student to see the prerequisites if the course is open to self-enrollment + if (!course.isEnrollmentEnabled()) { + authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, null); + } + + var prerequisites = prerequisiteRepository.findByCourseIdOrderByTitle(courseId); + + return ResponseEntity.ok(prerequisites.stream().map(PrerequisiteResponseDTO::of).toList()); + } + + /** + * POST /courses/:courseId/prerequisites : creates a new prerequisite competency. + * + * @param courseId the id of the course to which the competency should be added + * @param prerequisite the prerequisite that should be created + * @return the ResponseEntity with status 201 (Created) and with body the new prerequisite + * @throws URISyntaxException if the Location URI syntax is incorrect + */ + @PostMapping("courses/{courseId}/competencies/prerequisites") + @EnforceAtLeastEditorInCourse + public ResponseEntity createPrerequisite(@PathVariable long courseId, @RequestBody PrerequisiteRequestDTO prerequisite) throws URISyntaxException { + log.debug("REST request to create Prerequisite : {}", prerequisite); + + final var savedPrerequisite = prerequisiteService.createPrerequisite(prerequisite, courseId); + final var uri = new URI("/api/courses/" + courseId + "/prerequisites/" + savedPrerequisite.getId()); + + return ResponseEntity.created(uri).body(PrerequisiteResponseDTO.of(savedPrerequisite)); + } + + /** + * PUT /courses/:courseId/prerequisites/:prerequisiteId : updates an existing prerequisite + * + * @param courseId the id of the course to which the prerequisite belongs + * @param prerequisiteId the id of the prerequisite to update + * @param prerequisiteValues the new prerequisite values + * @return the ResponseEntity with status 200 (OK) + */ + @PutMapping("courses/{courseId}/competencies/prerequisites/{prerequisiteId}") + @EnforceAtLeastEditorInCourse + public ResponseEntity updatePrerequisite(@PathVariable long courseId, @PathVariable long prerequisiteId, + @RequestBody PrerequisiteRequestDTO prerequisiteValues) throws URISyntaxException { + log.info("REST request to update Prerequisite with id : {}", prerequisiteId); + + final var savedPrerequisite = prerequisiteService.updatePrerequisite(prerequisiteValues, prerequisiteId, courseId); + + return ResponseEntity.ok(PrerequisiteResponseDTO.of(savedPrerequisite)); + } + + /** + * DELETE /courses/:courseId/prerequisites/:prerequisiteId : deletes an existing prerequisite + * + * @param courseId the id of the course to which the prerequisite belongs + * @param prerequisiteId the id of the prerequisite to remove + * @return the ResponseEntity with status 200 (OK) + */ + @DeleteMapping("courses/{courseId}/competencies/prerequisites/{prerequisiteId}") + @EnforceAtLeastEditorInCourse + public ResponseEntity deletePrerequisite(@PathVariable long prerequisiteId, @PathVariable long courseId) { + log.info("REST request to delete Prerequisite with id : {}", prerequisiteId); + + prerequisiteService.deletePrerequisite(prerequisiteId, courseId); + + // TODO: add notification on the client-side. + return ResponseEntity.ok().build(); + } + + /** + * POST /courses/:courseId/prerequisites/import : imports a number of CourseCompetencies as Prerequisites + * + * @param courseId the id of the course to which the prerequisites should be imported to + * @param courseCompetencyIds the ids of the CourseCompetencies to import + * @return the ResponseEntity with status 201 (Created) and with body containing the imported prerequisites + * @throws URISyntaxException if the location URI syntax is incorrect + */ + @PostMapping("courses/{courseId}/prerequisites/import") + @EnforceAtLeastEditorInCourse + 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); + + return ResponseEntity.created(new URI("/api/courses/" + courseId + "/competencies/prerequisites")) + .body(importedPrerequisites.stream().map(PrerequisiteResponseDTO::of).toList()); + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteRequestDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteRequestDTO.java index b18954838d27..555bec9c6fe2 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteRequestDTO.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteRequestDTO.java @@ -7,14 +7,14 @@ import com.fasterxml.jackson.annotation.JsonInclude; -import de.tum.in.www1.artemis.domain.competency.AbstractCompetency; 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; /** * DTO used to send create/update requests regarding {@link Prerequisite} objects. */ @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record PrerequisiteRequestDTO(@NotBlank @Size(min = 1, max = AbstractCompetency.MAX_TITLE_LENGTH) String title, String description, CompetencyTaxonomy taxonomy, +public record PrerequisiteRequestDTO(@NotBlank @Size(min = 1, max = CourseCompetency.MAX_TITLE_LENGTH) String title, String description, CompetencyTaxonomy taxonomy, ZonedDateTime softDueDate, int masteryThreshold, boolean optional) { } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteResponseDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteResponseDTO.java index 2e6f86e2047f..30ac2397cddc 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteResponseDTO.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteResponseDTO.java @@ -11,10 +11,21 @@ * DTO used to send responses regarding {@link Prerequisite} objects. */ @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record PrerequisiteResponseDTO(long id, String title, String description, CompetencyTaxonomy taxonomy, ZonedDateTime softDueDate, int masteryThreshold, boolean optional) { +public record PrerequisiteResponseDTO(long id, String title, String description, CompetencyTaxonomy taxonomy, ZonedDateTime softDueDate, int masteryThreshold, boolean optional, + SourceCourseDTO sourceCourseDTO) { public static PrerequisiteResponseDTO of(Prerequisite prerequisite) { + SourceCourseDTO sourceCourseDTO = null; + if (prerequisite.getLinkedCourseCompetency() != null && prerequisite.getLinkedCourseCompetency().getCourse() != null) { + var course = prerequisite.getLinkedCourseCompetency().getCourse(); + sourceCourseDTO = new SourceCourseDTO(course.getId(), course.getTitle()); + } + return new PrerequisiteResponseDTO(prerequisite.getId(), prerequisite.getTitle(), prerequisite.getDescription(), prerequisite.getTaxonomy(), prerequisite.getSoftDueDate(), - prerequisite.getMasteryThreshold(), prerequisite.isOptional()); + prerequisite.getMasteryThreshold(), prerequisite.isOptional(), sourceCourseDTO); + } + + private record SourceCourseDTO(long id, String title) { + } } diff --git a/src/main/resources/config/liquibase/changelog/20240523180900_changelog.xml b/src/main/resources/config/liquibase/changelog/20240523180900_changelog.xml index 25e71d13437c..d1b91fdb6437 100644 --- a/src/main/resources/config/liquibase/changelog/20240523180900_changelog.xml +++ b/src/main/resources/config/liquibase/changelog/20240523180900_changelog.xml @@ -6,9 +6,16 @@ + + + + + UPDATE competency SET discriminator = 'COMPETENCY' - + + UPDATE competency SET mastery_threshold = 100 WHERE mastery_threshold IS NULL + SELECT @max_id := MAX(id) FROM competency; - INSERT INTO competency (id, description, title, course_id, taxonomy, mastery_threshold, soft_due_date, optional, linked_standardized_competency_id, discriminator) - SELECT (@max_id := @max_id + 1) as id, description, title, competency_course.course_id as course_id, taxonomy, mastery_threshold, soft_due_date, optional, linked_standardized_competency_id, 'PREREQUISITE' + INSERT INTO competency (id, description, title, course_id, taxonomy, mastery_threshold, soft_due_date, optional, linked_standardized_competency_id, discriminator, linked_competency_id) + SELECT (@max_id := @max_id + 1) as id, description, title, competency_course.course_id as course_id, taxonomy, mastery_threshold, soft_due_date, optional, linked_standardized_competency_id, 'PREREQUISITE', competency.id as linked_competency_id FROM competency RIGHT JOIN competency_course on competency.id = competency_course.competency_id; + diff --git a/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java b/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java index 210ecc9c868e..b846898883b7 100644 --- a/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java @@ -11,12 +11,14 @@ import de.tum.in.www1.artemis.domain.Exercise; import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.competency.CompetencyRelation; +import de.tum.in.www1.artemis.domain.competency.Prerequisite; import de.tum.in.www1.artemis.domain.competency.RelationType; import de.tum.in.www1.artemis.domain.lecture.LectureUnit; import de.tum.in.www1.artemis.repository.CompetencyRelationRepository; import de.tum.in.www1.artemis.repository.CompetencyRepository; import de.tum.in.www1.artemis.repository.ExerciseRepository; import de.tum.in.www1.artemis.repository.LectureUnitRepository; +import de.tum.in.www1.artemis.repository.PrerequisiteRepository; /** * Service responsible for initializing the database with specific testdata related to competencies for use in integration tests. @@ -36,6 +38,9 @@ public class CompetencyUtilService { @Autowired private CompetencyRelationRepository competencyRelationRepository; + @Autowired + private PrerequisiteRepository prerequisiteRepository; + /** * Creates and saves a Competency for the given Course. * @@ -168,4 +173,18 @@ public Competency updateMasteryThreshold(@NotNull Competency competency, int mas competency.setMasteryThreshold(masteryThreshold); return competencyRepo.save(competency); } + + /** + * Creates and saves a Prerequisite competency for the given Course. + * + * @param course The Course the Prerequisite belongs to + * @return The created Prerequisite + */ + public Prerequisite createPrerequisite(Course course) { + Prerequisite prerequisite = new Prerequisite(); + prerequisite.setTitle("Example Competency"); + prerequisite.setDescription("Magna pars studiorum, prodita quaerimus."); + prerequisite.setCourse(course); + return prerequisiteRepository.save(prerequisite); + } } 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 f66759843e21..4109562c394b 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 @@ -80,6 +80,7 @@ import de.tum.in.www1.artemis.domain.TextSubmission; 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.Prerequisite; import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; import de.tum.in.www1.artemis.domain.enumeration.ComplaintType; import de.tum.in.www1.artemis.domain.enumeration.CourseInformationSharingConfiguration; @@ -678,8 +679,8 @@ public void testEditCourseShouldPreserveAssociations() throws Exception { course.setCompetencies(competencies); course = courseRepo.save(course); - Set prerequisites = new HashSet<>(); - prerequisites.add(competencyUtilService.createCompetency(courseUtilService.createCourse())); + Set prerequisites = new HashSet<>(); + prerequisites.add(competencyUtilService.createPrerequisite(courseUtilService.createCourse())); course.setPrerequisites(prerequisites); course = courseRepo.save(course); 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 7dd0745c4748..ea9382175a49 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 @@ -168,7 +168,8 @@ void setupTestScenario() { course2 = courseUtilService.createCourse(); competency = createCompetency(course); - createPrerequisiteForCourse2(); + // TODO: re-enable later + // createPrerequisiteForCourse2(); lecture = createLecture(course); textExercise = createTextExercise(pastTimestamp, pastTimestamp, pastTimestamp, Set.of(competency), false); @@ -196,11 +197,14 @@ CompetencyRelation createRelation(Competency tail, Competency head, RelationType return competencyRelationRepository.save(relation); } - private void createPrerequisiteForCourse2() { - // Add the first competency as a prerequisite to the other course - course2.addPrerequisite(competency); - courseRepository.save(course2); - } + // TODO: re-enable later + /* + * private void createPrerequisiteForCourse2() { + * // Add the first competency as a prerequisite to the other course + * course2.addPrerequisite(competency); + * courseRepository.save(course2); + * } + */ private void creatingLectureUnitsOfLecture(Competency competency) { // creating lecture units for lecture one @@ -812,62 +816,56 @@ void getPrerequisitesShouldReturnPrerequisites() throws Exception { assertThat(prerequisites).containsExactly(competency); } - @Nested - class AddPrerequisite { - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void shouldAddPrerequisite() throws Exception { - Competency competency = new Competency(); - competency.setTitle("CompetencyTwo"); - competency.setDescription("This is an example competency"); - competency.setCourse(course2); - competency = competencyRepository.save(competency); - - Competency prerequisite = request.postWithResponseBody("/api/courses/" + course.getId() + "/prerequisites/" + competency.getId(), competency, Competency.class, - HttpStatus.OK); - - assertThat(prerequisite).isNotNull(); - assertThat(prerequisite.getConsecutiveCourses()).contains(course); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void shouldNotAddPrerequisiteWhenAlreadyCompetencyInCourse() throws Exception { - // Test that a competency of a course can not be a prerequisite to the same course - request.postWithResponseBody("/api/courses/" + course.getId() + "/prerequisites/" + competency.getId(), competency, Competency.class, HttpStatus.BAD_REQUEST); - } - } - - @Nested - class RemovePrerequisite { - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void shouldRemovePrerequisite() throws Exception { - request.delete("/api/courses/" + course2.getId() + "/prerequisites/" + competency.getId(), HttpStatus.OK); - Course course = courseRepository.findWithEagerCompetenciesById(course2.getId()).orElseThrow(); - assertThat(course.getPrerequisites()).doesNotContain(competency); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void shouldReturnBadRequestWhenPrerequisiteNotExists() throws Exception { - request.delete("/api/courses/" + course.getId() + "/prerequisites/" + competency.getId(), HttpStatus.BAD_REQUEST); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void shouldNotRemovePrerequisiteOfAnotherCourse() throws Exception { - Course anotherCourse = courseUtilService.createCourse(); - anotherCourse.addPrerequisite(competency); - anotherCourse = courseRepository.save(anotherCourse); - - request.delete("/api/courses/" + course2.getId() + "/prerequisites/" + competency.getId(), HttpStatus.OK); - anotherCourse = courseRepository.findWithEagerCompetenciesById(anotherCourse.getId()).orElseThrow(); - assertThat(anotherCourse.getPrerequisites()).contains(competency); - } - } + // TODO: re-enable when complete again + /* + * @Nested + * class AddPrerequisite { + * @Test + * @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + * void shouldAddPrerequisite() throws Exception { + * Competency competency = new Competency(); + * competency.setTitle("CompetencyTwo"); + * competency.setDescription("This is an example competency"); + * competency.setCourse(course2); + * competency = competencyRepository.save(competency); + * Competency prerequisite = request.postWithResponseBody("/api/courses/" + course.getId() + "/prerequisites/" + competency.getId(), competency, Competency.class, + * HttpStatus.OK); + * assertThat(prerequisite).isNotNull(); + * assertThat(prerequisite.getConsecutiveCourses()).contains(course); + * } + * @Test + * @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + * void shouldNotAddPrerequisiteWhenAlreadyCompetencyInCourse() throws Exception { + * // Test that a competency of a course can not be a prerequisite to the same course + * request.postWithResponseBody("/api/courses/" + course.getId() + "/prerequisites/" + competency.getId(), competency, Competency.class, HttpStatus.BAD_REQUEST); + * } + * } + * @Nested + * class RemovePrerequisite { + * @Test + * @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + * void shouldRemovePrerequisite() throws Exception { + * request.delete("/api/courses/" + course2.getId() + "/prerequisites/" + competency.getId(), HttpStatus.OK); + * Course course = courseRepository.findWithEagerCompetenciesById(course2.getId()).orElseThrow(); + * assertThat(course.getPrerequisites()).doesNotContain(competency); + * } + * @Test + * @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + * void shouldReturnBadRequestWhenPrerequisiteNotExists() throws Exception { + * request.delete("/api/courses/" + course.getId() + "/prerequisites/" + competency.getId(), HttpStatus.BAD_REQUEST); + * } + * @Test + * @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + * void shouldNotRemovePrerequisiteOfAnotherCourse() throws Exception { + * Course anotherCourse = courseUtilService.createCourse(); + * anotherCourse.addPrerequisite(competency); + * anotherCourse = courseRepository.save(anotherCourse); + * request.delete("/api/courses/" + course2.getId() + "/prerequisites/" + competency.getId(), HttpStatus.OK); + * anotherCourse = courseRepository.findWithEagerCompetenciesById(anotherCourse.getId()).orElseThrow(); + * assertThat(anotherCourse.getPrerequisites()).contains(competency); + * } + * } + */ @Nested class CreateCompetencies { From 3a182712cf74cb89c7738c62dff8b7410cfec769 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Sun, 26 May 2024 14:24:14 +0200 Subject: [PATCH 07/78] Remove unused methods from old competency import --- .../repository/CompetencyRepository.java | 22 ---------- .../repository/PrerequisiteRepository.java | 11 ----- .../service/competency/CompetencyService.java | 40 ------------------- .../artemis/web/rest/CompetencyResource.java | 15 ------- 4 files changed, 88 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 09458bdc28eb..b7dd4beb5b62 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 @@ -81,25 +81,6 @@ public interface CompetencyRepository extends JpaRepository, J """) 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. - * - * @param partialTitle competency title search term - * @param partialCourseTitle course title search term - * @param groups user groups - * @param pageable Pageable - * @return Page with search results - */ - @Query(""" - SELECT c - FROM Competency c - WHERE (c.course.instructorGroupName IN :groups OR c.course.editorGroupName IN :groups) - AND (c.title LIKE %:partialTitle% OR c.course.title LIKE %:partialCourseTitle%) - """) - Page findByTitleInLectureOrCourseAndUserHasAccessToCourse(@Param("partialTitle") String partialTitle, @Param("partialCourseTitle") String partialCourseTitle, - @Param("groups") Set groups, Pageable pageable); - /** * Query which fetches all competencies for which the user is editor or instructor in the course and * matching the search criteria. @@ -159,9 +140,6 @@ Page findForImport(@Param("partialTitle") String partialTitle, @Para @Cacheable(cacheNames = "competencyTitle", key = "#competencyId", unless = "#result == null") String getCompetencyTitle(@Param("competencyId") long competencyId); - @SuppressWarnings("PMD.MethodNamingConventions") - Page findByTitleIgnoreCaseContainingOrCourse_TitleIgnoreCaseContaining(String partialTitle, String partialCourseTitle, Pageable pageable); - default Competency findByIdWithLectureUnitsAndCompletionsElseThrow(long competencyId) { return findByIdWithLectureUnitsAndCompletions(competencyId).orElseThrow(() -> new EntityNotFoundException("Competency", competencyId)); } diff --git a/src/main/java/de/tum/in/www1/artemis/repository/PrerequisiteRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/PrerequisiteRepository.java index a885d6b297ac..1d4e1d754c1e 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/PrerequisiteRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/PrerequisiteRepository.java @@ -10,17 +10,6 @@ public interface PrerequisiteRepository extends JpaRepository { - // TODO: needed -> if yes we need to add linkedCompetency to prerequisite -> Would it make sense to also add to competency? - /* - * @Query(""" - * SELECT c - * FROM Competency c - * LEFT JOIN FETCH c.consecutiveCourses - * WHERE c.id = :competencyId - * """) - * Optional findByIdWithConsecutiveCourses(@Param("competencyId") long competencyId); - */ - List findByCourseIdOrderByTitle(long courseId); Long countByCourseId(long courseId); 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 b2fea17d9d29..bb1d56ad9f00 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 @@ -10,8 +10,6 @@ import java.util.Set; import java.util.stream.Collectors; -import jakarta.validation.constraints.NotNull; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Profile; @@ -40,7 +38,6 @@ import de.tum.in.www1.artemis.web.rest.dto.competency.CompetencyRelationDTO; import de.tum.in.www1.artemis.web.rest.dto.competency.CompetencyWithTailRelationDTO; import de.tum.in.www1.artemis.web.rest.dto.pageablesearch.CompetencyPageableSearchDTO; -import de.tum.in.www1.artemis.web.rest.dto.pageablesearch.SearchTermPageableSearchDTO; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; import de.tum.in.www1.artemis.web.rest.util.PageUtil; @@ -92,43 +89,6 @@ public CompetencyService(CompetencyRepository competencyRepository, Authorizatio this.courseRepository = courseRepository; } - /** - * Get all prerequisites for a course. Lecture units are removed. - * - * @param course The course for which the prerequisites should be retrieved. - * @return A list of prerequisites. - */ - public Set findAllPrerequisitesForCourse(@NotNull Course course) { - Set prerequisites = Set.of(); - // TODO: logic - // competencyRepository.findPrerequisitesByCourseId(course.getId()); - // Remove all lecture units - for (Competency prerequisite : prerequisites) { - prerequisite.setLectureUnits(Collections.emptySet()); - } - return prerequisites; - } - - /** - * Search for all competencies fitting a {@link SearchTermPageableSearchDTO search query}. The result is paged. - * - * @param search The search query defining the search term and the size of the returned page - * @param user The user for whom to the competencies - * @return A wrapper object containing a list of all found competencies and the total number of pages - */ - public SearchResultPageDTO getAllOnPageWithSize(final SearchTermPageableSearchDTO search, final User user) { - final var pageable = PageUtil.createDefaultPageRequest(search, PageUtil.ColumnMapping.COMPETENCY); - final var searchTerm = search.getSearchTerm(); - final Page competencyPage; - if (authCheckService.isAdmin(user)) { - competencyPage = competencyRepository.findByTitleIgnoreCaseContainingOrCourse_TitleIgnoreCaseContaining(searchTerm, searchTerm, pageable); - } - else { - competencyPage = competencyRepository.findByTitleInLectureOrCourseAndUserHasAccessToCourse(searchTerm, searchTerm, user.getGroups(), pageable); - } - return new SearchResultPageDTO<>(competencyPage.getContent(), competencyPage.getTotalPages()); - } - /** * Search for all competencies fitting a {@link CompetencyPageableSearchDTO search query}. The result is paged. * diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java index f03b154e48ee..b3f3ded99636 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java @@ -63,7 +63,6 @@ import de.tum.in.www1.artemis.web.rest.dto.competency.CompetencyRelationDTO; import de.tum.in.www1.artemis.web.rest.dto.competency.CompetencyWithTailRelationDTO; import de.tum.in.www1.artemis.web.rest.dto.pageablesearch.CompetencyPageableSearchDTO; -import de.tum.in.www1.artemis.web.rest.dto.pageablesearch.SearchTermPageableSearchDTO; import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; import de.tum.in.www1.artemis.web.rest.util.HeaderUtil; @@ -136,20 +135,6 @@ public ResponseEntity getCompetencyTitle(@PathVariable long competencyId return title == null ? ResponseEntity.notFound().build() : ResponseEntity.ok(title); } - // TODO (followup): this is only used for prerequisite import -> the prerequisite import to also use the new competency import. - /** - * Search for all competencies by title and course title. The result is pageable. - * - * @param search The pageable search containing the page size, page number and query string - * @return The desired page, sorted and matching the given query - */ - @GetMapping("competencies") - @EnforceAtLeastEditor - public ResponseEntity> getAllCompetenciesOnPage(SearchTermPageableSearchDTO search) { - final var user = userRepository.getUserWithGroupsAndAuthorities(); - return ResponseEntity.ok(competencyService.getAllOnPageWithSize(search, user)); - } - /** * Search for all competencies by title, description, course title and semester. The result is pageable. * From 0fd62e291567ca0f422eead57c08a1cf51414535 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Mon, 27 May 2024 00:53:39 +0200 Subject: [PATCH 08/78] Fix import --- .../in/www1/artemis/service/competency/CompetencyService.java | 1 + 1 file changed, 1 insertion(+) 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 5fc5a017f4b2..6818f6d61e49 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 @@ -10,6 +10,7 @@ import java.util.Set; import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Profile; From 4cac566ac4246b8e55c80368ddd2aee91e7fafb1 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Mon, 27 May 2024 22:36:06 +0200 Subject: [PATCH 09/78] Add server tests --- .../domain/competency/CourseCompetency.java | 4 +- .../repository/PrerequisiteRepository.java | 3 + .../www1/artemis/service/CourseService.java | 10 +- .../competency/PrerequisiteService.java | 3 +- .../rest/competency/PrerequisiteResource.java | 4 +- .../competency/CompetencyUtilService.java | 19 -- .../competency/PrerequisiteUtilService.java | 74 +++++++ .../artemis/course/CourseTestService.java | 6 +- .../lecture/CompetencyIntegrationTest.java | 76 ------- .../lecture/PrerequisiteIntegrationTest.java | 185 ++++++++++++++++++ 10 files changed, 280 insertions(+), 104 deletions(-) create mode 100644 src/test/java/de/tum/in/www1/artemis/competency/PrerequisiteUtilService.java create mode 100644 src/test/java/de/tum/in/www1/artemis/lecture/PrerequisiteIntegrationTest.java 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 2010741b3922..7153ec0b86cb 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 @@ -26,9 +26,9 @@ import de.tum.in.www1.artemis.domain.Course; -// TODO: javadoc for this. +// TODO: javadoc for this? @Entity -@Table(name = "competency") +@Table(name = "course_competency") @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "discriminator", discriminatorType = DiscriminatorType.STRING) @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/PrerequisiteRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/PrerequisiteRepository.java index 1d4e1d754c1e..1eed506a87bc 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/PrerequisiteRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/PrerequisiteRepository.java @@ -5,6 +5,7 @@ import org.springframework.data.jpa.repository.JpaRepository; +import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.competency.Prerequisite; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; @@ -27,4 +28,6 @@ default void existsByIdAndCourseIdElseThrow(long prerequisiteId, long courseId) throw new EntityNotFoundException("Prerequisite", prerequisiteId); } } + + long countByCourse(Course course); } 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 2e2457210f09..6eaf43494099 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 @@ -70,6 +70,7 @@ import de.tum.in.www1.artemis.repository.GroupNotificationRepository; import de.tum.in.www1.artemis.repository.LectureRepository; import de.tum.in.www1.artemis.repository.ParticipantScoreRepository; +import de.tum.in.www1.artemis.repository.PrerequisiteRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.RatingRepository; import de.tum.in.www1.artemis.repository.ResultRepository; @@ -145,6 +146,8 @@ public class CourseService { private final CompetencyRepository competencyRepository; + private final PrerequisiteRepository prerequisiteRepository; + private final GradingScaleRepository gradingScaleRepository; private final StatisticsRepository statisticsRepository; @@ -198,7 +201,8 @@ public CourseService(CourseRepository courseRepository, ExerciseService exercise ParticipantScoreRepository participantScoreRepository, PresentationPointsCalculationService presentationPointsCalculationService, TutorialGroupRepository tutorialGroupRepository, PlagiarismCaseRepository plagiarismCaseRepository, ConversationRepository conversationRepository, LearningPathService learningPathService, Optional irisSettingsService, LectureRepository lectureRepository, - TutorialGroupNotificationRepository tutorialGroupNotificationRepository, TutorialGroupChannelManagementService tutorialGroupChannelManagementService) { + TutorialGroupNotificationRepository tutorialGroupNotificationRepository, TutorialGroupChannelManagementService tutorialGroupChannelManagementService, + PrerequisiteRepository prerequisiteRepository) { this.courseRepository = courseRepository; this.exerciseService = exerciseService; this.exerciseDeletionService = exerciseDeletionService; @@ -236,6 +240,7 @@ public CourseService(CourseRepository courseRepository, ExerciseService exercise this.lectureRepository = lectureRepository; this.tutorialGroupNotificationRepository = tutorialGroupNotificationRepository; this.tutorialGroupChannelManagementService = tutorialGroupChannelManagementService; + this.prerequisiteRepository = prerequisiteRepository; } /** @@ -321,8 +326,7 @@ public Course findOneWithExercisesAndLecturesAndExamsAndCompetenciesAndTutorialG // NOTE: in this call we only want to know if competencies exist in the course, we will load them when the user navigates into them course.setNumberOfCompetencies(competencyRepository.countByCourse(course)); // NOTE: in this call we only want to know if prerequisites exist in the course, we will load them when the user navigates into them - // TODO: change this. - course.setNumberOfPrerequisites(0L); + course.setNumberOfPrerequisites(prerequisiteRepository.countByCourse(course)); // NOTE: in this call we only want to know if tutorial groups exist in the course, we will load them when the user navigates into them course.setNumberOfTutorialGroups(tutorialGroupRepository.countByCourse(course)); if (authCheckService.isOnlyStudentInCourse(course, user)) { 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 9b9df9b58551..063d0c47236b 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 @@ -99,8 +99,9 @@ public void deletePrerequisite(long prerequisiteId, long courseId) { * @return The list of imported prerequisites */ public List importPrerequisites(long courseId, List courseCompetencyIds) { + // TODO: check that we do not import any from the same course. var course = courseRepository.findByIdElseThrow(courseId); - var user = userRepository.getUser(); + var user = userRepository.getUserWithGroupsAndAuthorities(); List courseCompetenciesToImport; if (authorizationCheckService.isAdmin(user)) { courseCompetenciesToImport = courseCompetencyRepository.findAllByIdElseThrow(courseCompetencyIds); 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 b75ee6d71b5a..57229e5609e8 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 @@ -108,7 +108,7 @@ public ResponseEntity createPrerequisite(@PathVariable @PutMapping("courses/{courseId}/competencies/prerequisites/{prerequisiteId}") @EnforceAtLeastEditorInCourse public ResponseEntity updatePrerequisite(@PathVariable long courseId, @PathVariable long prerequisiteId, - @RequestBody PrerequisiteRequestDTO prerequisiteValues) throws URISyntaxException { + @RequestBody PrerequisiteRequestDTO prerequisiteValues) { log.info("REST request to update Prerequisite with id : {}", prerequisiteId); final var savedPrerequisite = prerequisiteService.updatePrerequisite(prerequisiteValues, prerequisiteId, courseId); @@ -142,7 +142,7 @@ public ResponseEntity deletePrerequisite(@PathVariable long prerequisiteId * @return the ResponseEntity with status 201 (Created) and with body containing the imported prerequisites * @throws URISyntaxException if the location URI syntax is incorrect */ - @PostMapping("courses/{courseId}/prerequisites/import") + @PostMapping("courses/{courseId}/competencies/prerequisites/import") @EnforceAtLeastEditorInCourse public ResponseEntity> importPrerequisites(@PathVariable long courseId, @RequestBody List courseCompetencyIds) throws URISyntaxException { log.info("REST request to import courseCompetencies with ids {} as prerequisites", courseCompetencyIds); diff --git a/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java b/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java index b846898883b7..210ecc9c868e 100644 --- a/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/competency/CompetencyUtilService.java @@ -11,14 +11,12 @@ import de.tum.in.www1.artemis.domain.Exercise; import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.competency.CompetencyRelation; -import de.tum.in.www1.artemis.domain.competency.Prerequisite; import de.tum.in.www1.artemis.domain.competency.RelationType; import de.tum.in.www1.artemis.domain.lecture.LectureUnit; import de.tum.in.www1.artemis.repository.CompetencyRelationRepository; import de.tum.in.www1.artemis.repository.CompetencyRepository; import de.tum.in.www1.artemis.repository.ExerciseRepository; import de.tum.in.www1.artemis.repository.LectureUnitRepository; -import de.tum.in.www1.artemis.repository.PrerequisiteRepository; /** * Service responsible for initializing the database with specific testdata related to competencies for use in integration tests. @@ -38,9 +36,6 @@ public class CompetencyUtilService { @Autowired private CompetencyRelationRepository competencyRelationRepository; - @Autowired - private PrerequisiteRepository prerequisiteRepository; - /** * Creates and saves a Competency for the given Course. * @@ -173,18 +168,4 @@ public Competency updateMasteryThreshold(@NotNull Competency competency, int mas competency.setMasteryThreshold(masteryThreshold); return competencyRepo.save(competency); } - - /** - * Creates and saves a Prerequisite competency for the given Course. - * - * @param course The Course the Prerequisite belongs to - * @return The created Prerequisite - */ - public Prerequisite createPrerequisite(Course course) { - Prerequisite prerequisite = new Prerequisite(); - prerequisite.setTitle("Example Competency"); - prerequisite.setDescription("Magna pars studiorum, prodita quaerimus."); - prerequisite.setCourse(course); - return prerequisiteRepository.save(prerequisite); - } } diff --git a/src/test/java/de/tum/in/www1/artemis/competency/PrerequisiteUtilService.java b/src/test/java/de/tum/in/www1/artemis/competency/PrerequisiteUtilService.java new file mode 100644 index 000000000000..7f511576f8b5 --- /dev/null +++ b/src/test/java/de/tum/in/www1/artemis/competency/PrerequisiteUtilService.java @@ -0,0 +1,74 @@ +package de.tum.in.www1.artemis.competency; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.competency.Prerequisite; +import de.tum.in.www1.artemis.repository.PrerequisiteRepository; +import de.tum.in.www1.artemis.web.rest.dto.competency.PrerequisiteRequestDTO; + +@Service +public class PrerequisiteUtilService { + + @Autowired + private PrerequisiteRepository prerequisiteRepository; + + /** + * Creates and saves a Prerequisite competency for the given Course. + * + * @param course The Course the Prerequisite belongs to + * @return The created Prerequisite + */ + public Prerequisite createPrerequisite(Course course) { + Prerequisite prerequisite = new Prerequisite(); + prerequisite.setTitle("Example Prerequisite"); + prerequisite.setDescription("Magna pars studiorum, prodita quaerimus."); + prerequisite.setCourse(course); + return prerequisiteRepository.save(prerequisite); + } + + /** + * Creates and saves a Prerequisite competency for the given Course. + * + * @param course The Course the Prerequisite belongs to + * @param suffix The suffix that will be added to the title of the Prerequisite + * @return The created Prerequisite + */ + private Prerequisite createPrerequisite(Course course, String suffix) { + Prerequisite prerequisite = new Prerequisite(); + prerequisite.setTitle("Example Prerequisite" + suffix); + prerequisite.setDescription("Magna pars studiorum, prodita quaerimus."); + prerequisite.setCourse(course); + return prerequisiteRepository.save(prerequisite); + } + + /** + * Creates and saves the given number of Prerequisites for the given Course. + * + * @param course The Course the Prerequisites belong to + * @param numberOfPrerequisites The number of Prerequisites to create + * @return A list of the created Prerequisites + */ + public List createPrerequisites(Course course, int numberOfPrerequisites) { + var prerequisites = new ArrayList(); + for (int i = 0; i < numberOfPrerequisites; i++) { + prerequisites.add(createPrerequisite(course, String.valueOf(i))); + } + return prerequisites; + } + + /** + * Creates a PrerequisiteRequestDTO from a prerequisite + * + * @param prerequisite the prerequisite to conver + * @return the created PrerequisiteRequestDTO + */ + public PrerequisiteRequestDTO prerequisiteToRequestDTO(Prerequisite prerequisite) { + return new PrerequisiteRequestDTO(prerequisite.getTitle(), prerequisite.getDescription(), prerequisite.getTaxonomy(), prerequisite.getSoftDueDate(), + prerequisite.getMasteryThreshold(), prerequisite.isOptional()); + } +} 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 4109562c394b..9b17f390d7e4 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 @@ -62,6 +62,7 @@ import de.tum.in.www1.artemis.assessment.ComplaintUtilService; import de.tum.in.www1.artemis.competency.CompetencyUtilService; +import de.tum.in.www1.artemis.competency.PrerequisiteUtilService; import de.tum.in.www1.artemis.config.Constants; import de.tum.in.www1.artemis.domain.Complaint; import de.tum.in.www1.artemis.domain.ComplaintResponse; @@ -272,6 +273,9 @@ public class CourseTestService { @Autowired private CompetencyUtilService competencyUtilService; + @Autowired + private PrerequisiteUtilService prerequisiteUtilService; + @Autowired private LectureUtilService lectureUtilService; @@ -680,7 +684,7 @@ public void testEditCourseShouldPreserveAssociations() throws Exception { course = courseRepo.save(course); Set prerequisites = new HashSet<>(); - prerequisites.add(competencyUtilService.createPrerequisite(courseUtilService.createCourse())); + prerequisites.add(prerequisiteUtilService.createPrerequisite(courseUtilService.createCourse())); course.setPrerequisites(prerequisites); course = courseRepo.save(course); 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 ea9382175a49..c446c1615a7f 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 @@ -50,7 +50,6 @@ import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.repository.CompetencyRelationRepository; import de.tum.in.www1.artemis.repository.CompetencyRepository; -import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.ExerciseRepository; import de.tum.in.www1.artemis.repository.ExerciseUnitRepository; import de.tum.in.www1.artemis.repository.LectureRepository; @@ -74,9 +73,6 @@ class CompetencyIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCT private static final String TEST_PREFIX = "competencyintegrationtest"; - @Autowired - private CourseRepository courseRepository; - @Autowired private LectureRepository lectureRepository; @@ -168,8 +164,6 @@ void setupTestScenario() { course2 = courseUtilService.createCourse(); competency = createCompetency(course); - // TODO: re-enable later - // createPrerequisiteForCourse2(); lecture = createLecture(course); textExercise = createTextExercise(pastTimestamp, pastTimestamp, pastTimestamp, Set.of(competency), false); @@ -197,15 +191,6 @@ CompetencyRelation createRelation(Competency tail, Competency head, RelationType return competencyRelationRepository.save(relation); } - // TODO: re-enable later - /* - * private void createPrerequisiteForCourse2() { - * // Add the first competency as a prerequisite to the other course - * course2.addPrerequisite(competency); - * courseRepository.save(course2); - * } - */ - private void creatingLectureUnitsOfLecture(Competency competency) { // creating lecture units for lecture one @@ -303,9 +288,6 @@ private void testAllPreAuthorizeInstructor() throws Exception { // import request.post("/api/courses/" + course.getId() + "/competencies/import-all/1", null, HttpStatus.FORBIDDEN); request.post("/api/courses/" + course.getId() + "/competencies/import", competency, HttpStatus.FORBIDDEN); - // prerequisites - request.post("/api/courses/" + course.getId() + "/prerequisites/1", null, HttpStatus.FORBIDDEN); - request.delete("/api/courses/" + course.getId() + "/prerequisites/1", HttpStatus.FORBIDDEN); // relations request.post("/api/courses/" + course.getId() + "/competencies/relations", new CompetencyRelation(), HttpStatus.FORBIDDEN); request.getSet("/api/courses/" + course.getId() + "/competencies/relations", HttpStatus.FORBIDDEN, CompetencyRelationDTO.class); @@ -809,64 +791,6 @@ void shouldGetResultsFromAllCoursesForAdmin() throws Exception { } } - @Test - @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void getPrerequisitesShouldReturnPrerequisites() throws Exception { - List prerequisites = request.getList("/api/courses/" + course2.getId() + "/prerequisites", HttpStatus.OK, Competency.class); - assertThat(prerequisites).containsExactly(competency); - } - - // TODO: re-enable when complete again - /* - * @Nested - * class AddPrerequisite { - * @Test - * @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - * void shouldAddPrerequisite() throws Exception { - * Competency competency = new Competency(); - * competency.setTitle("CompetencyTwo"); - * competency.setDescription("This is an example competency"); - * competency.setCourse(course2); - * competency = competencyRepository.save(competency); - * Competency prerequisite = request.postWithResponseBody("/api/courses/" + course.getId() + "/prerequisites/" + competency.getId(), competency, Competency.class, - * HttpStatus.OK); - * assertThat(prerequisite).isNotNull(); - * assertThat(prerequisite.getConsecutiveCourses()).contains(course); - * } - * @Test - * @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - * void shouldNotAddPrerequisiteWhenAlreadyCompetencyInCourse() throws Exception { - * // Test that a competency of a course can not be a prerequisite to the same course - * request.postWithResponseBody("/api/courses/" + course.getId() + "/prerequisites/" + competency.getId(), competency, Competency.class, HttpStatus.BAD_REQUEST); - * } - * } - * @Nested - * class RemovePrerequisite { - * @Test - * @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - * void shouldRemovePrerequisite() throws Exception { - * request.delete("/api/courses/" + course2.getId() + "/prerequisites/" + competency.getId(), HttpStatus.OK); - * Course course = courseRepository.findWithEagerCompetenciesById(course2.getId()).orElseThrow(); - * assertThat(course.getPrerequisites()).doesNotContain(competency); - * } - * @Test - * @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - * void shouldReturnBadRequestWhenPrerequisiteNotExists() throws Exception { - * request.delete("/api/courses/" + course.getId() + "/prerequisites/" + competency.getId(), HttpStatus.BAD_REQUEST); - * } - * @Test - * @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - * void shouldNotRemovePrerequisiteOfAnotherCourse() throws Exception { - * Course anotherCourse = courseUtilService.createCourse(); - * anotherCourse.addPrerequisite(competency); - * anotherCourse = courseRepository.save(anotherCourse); - * request.delete("/api/courses/" + course2.getId() + "/prerequisites/" + competency.getId(), HttpStatus.OK); - * anotherCourse = courseRepository.findWithEagerCompetenciesById(anotherCourse.getId()).orElseThrow(); - * assertThat(anotherCourse.getPrerequisites()).contains(competency); - * } - * } - */ - @Nested class CreateCompetencies { 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 new file mode 100644 index 000000000000..4b10f1169963 --- /dev/null +++ b/src/test/java/de/tum/in/www1/artemis/lecture/PrerequisiteIntegrationTest.java @@ -0,0 +1,185 @@ +package de.tum.in.www1.artemis.lecture; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +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.AbstractSpringIntegrationIndependentTest; +import de.tum.in.www1.artemis.competency.PrerequisiteUtilService; +import de.tum.in.www1.artemis.course.CourseUtilService; +import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.DomainObject; +import de.tum.in.www1.artemis.domain.competency.CompetencyTaxonomy; +import de.tum.in.www1.artemis.domain.competency.Prerequisite; +import de.tum.in.www1.artemis.repository.CourseRepository; +import de.tum.in.www1.artemis.repository.PrerequisiteRepository; +import de.tum.in.www1.artemis.user.UserUtilService; +import de.tum.in.www1.artemis.web.rest.dto.competency.PrerequisiteResponseDTO; + +public class PrerequisiteIntegrationTest extends AbstractSpringIntegrationIndependentTest { + + private static final String TEST_PREFIX = "prerequisiteintegrationtest"; + + @Autowired + private UserUtilService userUtilService; + + @Autowired + private CourseUtilService courseUtilService; + + @Autowired + private PrerequisiteUtilService prerequisiteUtilService; + + @Autowired + private PrerequisiteRepository prerequisiteRepository; + + @Autowired + private CourseRepository courseRepository; + + private Course course; + + private Course course2; + + @BeforeEach + void setupTestScenario() { + userUtilService.addUsers(TEST_PREFIX, 1, 1, 1, 1); + // users not in courses + userUtilService.createAndSaveUser(TEST_PREFIX + "student42"); + userUtilService.createAndSaveUser(TEST_PREFIX + "instructor42"); + + // creating courses + course = courseUtilService.createCourse(); + course2 = courseUtilService.createCourse(); + } + + private static String baseUrl(long courseId) { + return "/api/courses/" + courseId + "/competencies/prerequisites"; + } + + @Nested + class GetPrerequisites { + + private static String url(long courseId) { + return PrerequisiteIntegrationTest.baseUrl(courseId); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void shouldReturnPrerequisites() throws Exception { + prerequisiteUtilService.createPrerequisites(course, 5); + var prerequisites = prerequisiteRepository.findByCourseIdOrderByTitle(course.getId()); + var expectedPrerequisites = prerequisites.stream().map(PrerequisiteResponseDTO::of).toList(); + + List actualPrerequisites = request.getList(url(course.getId()), HttpStatus.OK, PrerequisiteResponseDTO.class); + + assertThat(actualPrerequisites).containsAll(expectedPrerequisites); + assertThat(actualPrerequisites).hasSameSizeAs(expectedPrerequisites); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student42", roles = "USER") + void shouldReturnPrerequisitesForStudentNotInCourseIfEnrollmentIsEnabled() throws Exception { + course.setEnrollmentEnabled(true); + courseRepository.save(course); + prerequisiteUtilService.createPrerequisites(course, 5); + var prerequisites = prerequisiteRepository.findByCourseIdOrderByTitle(course.getId()); + var expectedPrerequisites = prerequisites.stream().map(PrerequisiteResponseDTO::of).toList(); + + List actualPrerequisites = request.getList(url(course.getId()), HttpStatus.OK, PrerequisiteResponseDTO.class); + + assertThat(actualPrerequisites).containsAll(expectedPrerequisites); + assertThat(actualPrerequisites).hasSameSizeAs(expectedPrerequisites); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student42", roles = "USER") + void shouldNotReturnPrerequisitesForStudentNotInCourse() throws Exception { + request.get(url(course2.getId()), HttpStatus.FORBIDDEN, PrerequisiteResponseDTO.class); + } + } + + @Nested + class CreatePrerequisite { + + private static String url(long courseId) { + return PrerequisiteIntegrationTest.baseUrl(courseId); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") + void shouldCreatePrerequisite() throws Exception { + var prerequisite = new Prerequisite("title", "description", null, 66, CompetencyTaxonomy.ANALYZE, false); + var requestBody = prerequisiteUtilService.prerequisiteToRequestDTO(prerequisite); + + var actualPrerequisite = request.postWithResponseBody(url(course.getId()), requestBody, PrerequisiteResponseDTO.class, HttpStatus.CREATED); + + var expectedPrerequisite = new PrerequisiteResponseDTO(actualPrerequisite.id(), prerequisite.getTitle(), prerequisite.getDescription(), prerequisite.getTaxonomy(), + prerequisite.getSoftDueDate(), prerequisite.getMasteryThreshold(), prerequisite.isOptional(), null); + assertThat(actualPrerequisite).usingRecursiveComparison().ignoringFields("id").isEqualTo(expectedPrerequisite); + } + + } + + @Nested + class DeletePrerequisite { + + private static String url(long courseId, long prerequisiteId) { + return PrerequisiteIntegrationTest.baseUrl(courseId) + "/" + prerequisiteId; + } + + @Test + @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") + void shouldDeletePrerequisite() throws Exception { + var prerequisite = prerequisiteUtilService.createPrerequisite(course); + + request.delete(url(course.getId(), prerequisite.getId()), HttpStatus.OK); + + boolean exists = prerequisiteRepository.existsById(prerequisite.getId()); + assertThat(exists).isFalse(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") + 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); + + boolean exists = prerequisiteRepository.existsById(prerequisite.getId()); + assertThat(exists).isTrue(); + } + } + + @Nested + class ImportPrerequisites { + + private static String url(long courseId) { + return PrerequisiteIntegrationTest.baseUrl(courseId) + "/import"; + } + + @Test + @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") + void shouldImportPrerequisites() throws Exception { + var prerequisites = prerequisiteUtilService.createPrerequisites(course, 5); + var prerequisiteIds = prerequisites.stream().map(DomainObject::getId).toList(); + + var expectedPrerequisites = prerequisites.stream().map(prerequisite -> { + prerequisite.setLinkedCourseCompetency(prerequisite); + return PrerequisiteResponseDTO.of(prerequisite); + }).toList(); + + var actualPrerequisites = request.postListWithResponseBody(url(course2.getId()), prerequisiteIds, PrerequisiteResponseDTO.class, HttpStatus.CREATED); + + assertThat(actualPrerequisites).usingRecursiveFieldByFieldElementComparatorIgnoringFields("id").containsAll(expectedPrerequisites); + assertThat(actualPrerequisites).hasSameSizeAs(expectedPrerequisites); + } + + } +} From e9829b26d32b00a09b1705d0b92f9127d13efa54 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Tue, 28 May 2024 13:19:13 +0200 Subject: [PATCH 10/78] Improve import, add test for update prerequisite --- .../competency/PrerequisiteService.java | 11 +++++- .../rest/competency/PrerequisiteResource.java | 2 -- .../lecture/PrerequisiteIntegrationTest.java | 34 +++++++++++++++++++ 3 files changed, 44 insertions(+), 3 deletions(-) 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 063d0c47236b..994458dac92a 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 @@ -4,6 +4,9 @@ import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; + +import jakarta.ws.rs.BadRequestException; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -74,6 +77,7 @@ public Prerequisite updatePrerequisite(PrerequisiteRequestDTO prerequisiteValues existingPrerequisite.setTitle(prerequisiteValues.title()); existingPrerequisite.setDescription(prerequisiteValues.description()); existingPrerequisite.setTaxonomy(prerequisiteValues.taxonomy()); + existingPrerequisite.setSoftDueDate(prerequisiteValues.softDueDate()); existingPrerequisite.setMasteryThreshold(prerequisiteValues.masteryThreshold()); existingPrerequisite.setOptional(prerequisiteValues.optional()); @@ -99,7 +103,6 @@ public void deletePrerequisite(long prerequisiteId, long courseId) { * @return The list of imported prerequisites */ public List importPrerequisites(long courseId, List courseCompetencyIds) { - // TODO: check that we do not import any from the same course. var course = courseRepository.findByIdElseThrow(courseId); var user = userRepository.getUserWithGroupsAndAuthorities(); List courseCompetenciesToImport; @@ -107,9 +110,15 @@ public List importPrerequisites(long courseId, List courseCo courseCompetenciesToImport = courseCompetencyRepository.findAllByIdElseThrow(courseCompetencyIds); } else { + // only allow the user to import from courses where they are at least editor courseCompetenciesToImport = courseCompetencyRepository.findAllByIdAndUserIsAtLeastEditorInCourseElseThrow(courseCompetencyIds, user.getGroups()); } + var courseIds = courseCompetenciesToImport.stream().map(c -> c.getCourse().getId()).collect(Collectors.toSet()); + if (courseIds.contains(course.getId())) { + 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(); 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 57229e5609e8..8521c324a99e 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 @@ -38,8 +38,6 @@ public class PrerequisiteResource { private static final Logger log = LoggerFactory.getLogger(CompetencyResource.class); - private static final String ENTITY_NAME = "prerequisite"; - private final PrerequisiteService prerequisiteService; private final PrerequisiteRepository prerequisiteRepository; 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 4b10f1169963..72cbae0b704f 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 @@ -21,6 +21,7 @@ import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.PrerequisiteRepository; import de.tum.in.www1.artemis.user.UserUtilService; +import de.tum.in.www1.artemis.web.rest.dto.competency.PrerequisiteRequestDTO; import de.tum.in.www1.artemis.web.rest.dto.competency.PrerequisiteResponseDTO; public class PrerequisiteIntegrationTest extends AbstractSpringIntegrationIndependentTest { @@ -126,6 +127,28 @@ void shouldCreatePrerequisite() throws Exception { } + @Nested + class UpdatePrerequisite { + + private static String url(long courseId, long prerequisiteId) { + return PrerequisiteIntegrationTest.baseUrl(courseId) + "/" + prerequisiteId; + } + + @Test + @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") + void shouldUpdatePrerequisite() throws Exception { + var existingPrerequisite = prerequisiteUtilService.createPrerequisite(course); + var updateDTO = new PrerequisiteRequestDTO("new title", "new description", CompetencyTaxonomy.ANALYZE, null, 1, true); + + var updatedPrerequisite = request.putWithResponseBody(url(course.getId(), existingPrerequisite.getId()), updateDTO, PrerequisiteResponseDTO.class, HttpStatus.OK); + + assertThat(updatedPrerequisite).usingRecursiveComparison().comparingOnlyFields("title", "description", "taxonomy", "softDueDate", "masteryThreshold", "optional") + .isEqualTo(updateDTO); + assertThat(updatedPrerequisite.id()).isEqualTo(existingPrerequisite.getId()); + } + + } + @Nested class DeletePrerequisite { @@ -181,5 +204,16 @@ void shouldImportPrerequisites() throws Exception { assertThat(actualPrerequisites).hasSameSizeAs(expectedPrerequisites); } + @Test + @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") + void shouldReturnBadRequestWhenImportingFromSameCourse() throws Exception { + var prerequisites = prerequisiteUtilService.createPrerequisites(course, 4); + // one of the competencies is in the same course we import into (course2) + prerequisites.add(prerequisiteUtilService.createPrerequisite(course2)); + var prerequisiteIds = prerequisites.stream().map(DomainObject::getId).toList(); + + request.post(url(course2.getId()), prerequisiteIds, HttpStatus.BAD_REQUEST); + } + } } From a95a08e7a14b00b3ea6924335bbcc0f404a19408 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Tue, 28 May 2024 13:46:15 +0200 Subject: [PATCH 11/78] Change competency model to be an interface --- .../create-competency.component.ts | 4 +- .../webapp/app/entities/competency.model.ts | 37 ++++++++++-------- .../standardized-competency.model.ts | 6 +-- src/main/webapp/app/entities/course.model.ts | 1 + .../webapp/app/entities/prerequisite.model.ts | 3 ++ .../lecture-wizard-competencies.component.ts | 6 +-- .../competency-form.component.spec.ts | 2 +- .../competency-management.component.spec.ts | 2 +- .../competency-popover.component.spec.ts | 5 +-- .../competencies/competency.service.spec.ts | 2 +- .../course-competencies.component.spec.ts | 8 ++-- .../create-competency.component.spec.ts | 4 +- .../edit-competency.component.spec.ts | 4 +- .../generate-competencies.component.spec.ts | 2 +- ...urse-prerequisites-modal.component.spec.ts | 2 +- .../course/course-overview.component.spec.ts | 5 +-- .../competency-node-details.component.spec.ts | 2 +- ...ture-wizard-competencies.component.spec.ts | 38 +++++++++---------- 18 files changed, 69 insertions(+), 64 deletions(-) create mode 100644 src/main/webapp/app/entities/prerequisite.model.ts 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 4d9108076081..14b2e3e83cd9 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 = new Competency(); + competencyToCreate: Competency = {}; isLoading: boolean; courseId: number; lecturesWithLectureUnits: Lecture[] = []; @@ -32,7 +32,7 @@ export class CreateCompetencyComponent implements OnInit { ) {} ngOnInit(): void { - this.competencyToCreate = new Competency(); + this.competencyToCreate = {}; this.isLoading = true; this.activatedRoute .parent!.parent!.paramMap.pipe( diff --git a/src/main/webapp/app/entities/competency.model.ts b/src/main/webapp/app/entities/competency.model.ts index f372af374f72..0b17eb52e404 100644 --- a/src/main/webapp/app/entities/competency.model.ts +++ b/src/main/webapp/app/entities/competency.model.ts @@ -37,24 +37,29 @@ export enum CompetencyValidators { DESCRIPTION_MAX = 10000, } -export const DEFAULT_MASTERY_THRESHOLD = 50; +export const DEFAULT_MASTERY_THRESHOLD = 100; -export class Competency implements BaseEntity { - public id?: number; - public title?: string; - public description?: string; - public softDueDate?: dayjs.Dayjs; - public taxonomy?: CompetencyTaxonomy; - public masteryThreshold?: number; - public optional?: boolean; - public course?: Course; - public exercises?: Exercise[]; - public lectureUnits?: LectureUnit[]; - public userProgress?: CompetencyProgress[]; - public courseProgress?: CourseCompetencyProgress; - public linkedStandardizedCompetency?: StandardizedCompetency; +export interface BaseCompetency extends BaseEntity { + title?: string; + description?: string; + taxonomy?: CompetencyTaxonomy; +} - constructor() {} +export interface CourseCompetency extends BaseCompetency { + softDueDate?: dayjs.Dayjs; + masteryThreshold?: number; + optional?: boolean; + course?: Course; + linkedStandardizedCompetency?: StandardizedCompetency; + linkedCourseCompetency?: CourseCompetency; +} + +export interface Competency extends CourseCompetency { + exercises?: Exercise[]; + lectureUnits?: LectureUnit[]; + userProgress?: CompetencyProgress[]; + courseProgress?: CourseCompetencyProgress; + linkedStandardizedCompetency?: StandardizedCompetency; } export interface CompetencyImportResponseDTO extends BaseEntity { 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 d6cae87e554b..303098d12abd 100644 --- a/src/main/webapp/app/entities/competency/standardized-competency.model.ts +++ b/src/main/webapp/app/entities/competency/standardized-competency.model.ts @@ -1,10 +1,8 @@ import { Competency, CompetencyTaxonomy } from 'app/entities/competency.model'; import { BaseEntity } from 'app/shared/model/base-entity'; +import { BaseCompetency } from 'app/entities/competency.model'; -export interface StandardizedCompetency extends BaseEntity { - title?: string; - description?: string; - taxonomy?: CompetencyTaxonomy; +export interface StandardizedCompetency extends BaseCompetency { version?: string; knowledgeArea?: KnowledgeArea; source?: Source; diff --git a/src/main/webapp/app/entities/course.model.ts b/src/main/webapp/app/entities/course.model.ts index a8937917dc5d..9ae5cff47a1c 100644 --- a/src/main/webapp/app/entities/course.model.ts +++ b/src/main/webapp/app/entities/course.model.ts @@ -109,6 +109,7 @@ export class Course implements BaseEntity { public exercises?: Exercise[]; public lectures?: Lecture[]; public competencies?: Competency[]; + //TODO: change here aswell! public prerequisites?: Competency[]; public learningPathsEnabled?: boolean; public learningPaths?: LearningPath[]; diff --git a/src/main/webapp/app/entities/prerequisite.model.ts b/src/main/webapp/app/entities/prerequisite.model.ts new file mode 100644 index 000000000000..6e0c63e128e8 --- /dev/null +++ b/src/main/webapp/app/entities/prerequisite.model.ts @@ -0,0 +1,3 @@ +import { CourseCompetency } from 'app/entities/competency.model'; + +export interface Prerequisite extends CourseCompetency {} diff --git a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-competencies.component.ts b/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-competencies.component.ts index c3a419923aaa..e8f6f6e2fcc4 100644 --- a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-competencies.component.ts +++ b/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-competencies.component.ts @@ -124,7 +124,7 @@ export class LectureUpdateWizardCompetenciesComponent implements OnInit { } const { title, description, taxonomy, connectedLectureUnits } = formData; - this.currentlyProcessedCompetency = new Competency(); + this.currentlyProcessedCompetency = {}; this.currentlyProcessedCompetency.title = title; this.currentlyProcessedCompetency.description = description; @@ -198,7 +198,7 @@ export class LectureUpdateWizardCompetenciesComponent implements OnInit { } this.alertService.success(`Competency ${this.currentlyProcessedCompetency.title} was successfully edited.`); - this.currentlyProcessedCompetency = new Competency(); + this.currentlyProcessedCompetency = {}; }, error: (res: HttpErrorResponse) => onError(this.alertService, res), }); @@ -267,6 +267,6 @@ export class LectureUpdateWizardCompetenciesComponent implements OnInit { this.isConnectingCompetency = false; this.isLoadingCompetencyForm = false; - this.currentlyProcessedCompetency = new Competency(); + this.currentlyProcessedCompetency = {}; } } diff --git a/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts b/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts index dd6f56093291..f60e3fa7cde4 100644 --- a/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts @@ -60,7 +60,7 @@ describe('CompetencyFormComponent', () => { // stubbing competency service for asynchronous validator const competencyService = TestBed.inject(CompetencyService); - const competencyOfResponse = new Competency(); + const competencyOfResponse: Competency = {}; competencyOfResponse.id = 1; competencyOfResponse.title = 'test'; diff --git a/src/test/javascript/spec/component/competencies/competency-management/competency-management.component.spec.ts b/src/test/javascript/spec/component/competencies/competency-management/competency-management.component.spec.ts index 99be0557b71d..78ec80fd7d24 100644 --- a/src/test/javascript/spec/component/competencies/competency-management/competency-management.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/competency-management/competency-management.component.spec.ts @@ -82,7 +82,7 @@ describe('CompetencyManagementComponent', () => { competencyService = TestBed.inject(CompetencyService); modalService = fixture.debugElement.injector.get(NgbModal); - const competency = new Competency(); + const competency: Competency = {}; const textUnit = new TextUnit(); competency.id = 1; competency.description = 'test'; diff --git a/src/test/javascript/spec/component/competencies/competency-popover.component.spec.ts b/src/test/javascript/spec/component/competencies/competency-popover.component.spec.ts index fc9751fa6dc7..95d919c34ec0 100644 --- a/src/test/javascript/spec/component/competencies/competency-popover.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/competency-popover.component.spec.ts @@ -6,7 +6,6 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { RouterTestingModule } from '@angular/router/testing'; import { CompetenciesPopoverComponent } from 'app/course/competencies/competencies-popover/competencies-popover.component'; import { By } from '@angular/platform-browser'; -import { Competency } from 'app/entities/competency.model'; import { Component } from '@angular/core'; import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'; @@ -58,7 +57,7 @@ describe('CompetencyPopoverComponent', () => { it('should navigate to course competencies', fakeAsync(() => { const location: Location = TestBed.inject(Location); competencyPopoverComponent.navigateTo = 'courseCompetencies'; - competencyPopoverComponent.competencies = [new Competency()]; + competencyPopoverComponent.competencies = [{}]; competencyPopoverComponent.courseId = 1; competencyPopoverComponentFixture.detectChanges(); const popoverButton = competencyPopoverComponentFixture.debugElement.nativeElement.querySelector('button'); @@ -73,7 +72,7 @@ describe('CompetencyPopoverComponent', () => { it('should navigate to competency management', fakeAsync(() => { const location: Location = TestBed.inject(Location); competencyPopoverComponent.navigateTo = 'competencyManagement'; - competencyPopoverComponent.competencies = [new Competency()]; + competencyPopoverComponent.competencies = [{}]; competencyPopoverComponent.courseId = 1; competencyPopoverComponentFixture.detectChanges(); const popoverButton = competencyPopoverComponentFixture.debugElement.nativeElement.querySelector('button'); diff --git a/src/test/javascript/spec/component/competencies/competency.service.spec.ts b/src/test/javascript/spec/component/competencies/competency.service.spec.ts index d18017418a1b..911c685ea3f1 100644 --- a/src/test/javascript/spec/component/competencies/competency.service.spec.ts +++ b/src/test/javascript/spec/component/competencies/competency.service.spec.ts @@ -140,7 +140,7 @@ describe('CompetencyService', () => { const returnedFromService = { ...defaultCompetencies.first(), id: 0 }; const expected = { ...returnedFromService }; competencyService - .create(new Competency(), 1) + .create({}, 1) .pipe(take(1)) .subscribe((resp) => (expectedResultCompetency = resp)); diff --git a/src/test/javascript/spec/component/competencies/course-competencies.component.spec.ts b/src/test/javascript/spec/component/competencies/course-competencies.component.spec.ts index 7da50e313c3e..01231c44879f 100644 --- a/src/test/javascript/spec/component/competencies/course-competencies.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/course-competencies.component.spec.ts @@ -87,7 +87,7 @@ describe('CourseCompetencies', () => { it('should load progress for each competency in a given course', () => { const courseStorageService = TestBed.inject(CourseStorageService); - const competency = new Competency(); + const competency: Competency = {}; competency.userProgress = [{ progress: 70, confidence: 45 } as CompetencyProgress]; const textUnit = new TextUnit(); competency.id = 1; @@ -114,7 +114,7 @@ describe('CourseCompetencies', () => { }); it('should load prerequisites and competencies (with associated progress) and display a card for each of them', () => { - const competency = new Competency(); + const competency: Competency = {}; const textUnit = new TextUnit(); competency.id = 1; competency.description = 'test'; @@ -122,11 +122,11 @@ describe('CourseCompetencies', () => { competency.userProgress = [{ progress: 70, confidence: 45 } as CompetencyProgress]; const prerequisitesOfCourseResponse: HttpResponse = new HttpResponse({ - body: [new Competency()], + body: [{}], status: 200, }); const competenciesOfCourseResponse: HttpResponse = new HttpResponse({ - body: [competency, new Competency()], + body: [competency, {}], status: 200, }); 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 9dde7ace7b71..b9bf38de4a8d 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 @@ -79,7 +79,7 @@ describe('CreateCompetency', () => { }; const response: HttpResponse = new HttpResponse({ - body: new Competency(), + body: {}, status: 201, }); @@ -92,7 +92,7 @@ describe('CreateCompetency', () => { competencyForm.formSubmitted.emit(formData); return createCompetencyComponentFixture.whenStable().then(() => { - const competency = new Competency(); + const competency: Competency = {}; competency.title = formData.title; competency.description = formData.description; competency.optional = formData.optional; diff --git a/src/test/javascript/spec/component/competencies/edit-competency.component.spec.ts b/src/test/javascript/spec/component/competencies/edit-competency.component.spec.ts index 091cccb4561f..3842418fd9a2 100644 --- a/src/test/javascript/spec/component/competencies/edit-competency.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/edit-competency.component.spec.ts @@ -78,7 +78,7 @@ describe('EditCompetencyComponent', () => { const lectureUnit = new TextUnit(); lectureUnit.id = 1; - const competencyOfResponse = new Competency(); + const competencyOfResponse: Competency = {}; competencyOfResponse.id = 1; competencyOfResponse.title = 'test'; competencyOfResponse.description = 'lorem ipsum'; @@ -134,7 +134,7 @@ describe('EditCompetencyComponent', () => { const textUnit = new TextUnit(); textUnit.id = 1; - const competencyDatabase: Competency = new Competency(); + const competencyDatabase: Competency = {}; competencyDatabase.id = 1; competencyDatabase.title = 'test'; competencyDatabase.description = 'lorem ipsum'; diff --git a/src/test/javascript/spec/component/competencies/generate-competencies/generate-competencies.component.spec.ts b/src/test/javascript/spec/component/competencies/generate-competencies/generate-competencies.component.spec.ts index 30501bc81511..ff3e4a6b9fb2 100644 --- a/src/test/javascript/spec/component/competencies/generate-competencies/generate-competencies.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/generate-competencies/generate-competencies.component.spec.ts @@ -82,7 +82,7 @@ describe('GenerateCompetenciesComponent', () => { generateCompetenciesComponentFixture.detectChanges(); const courseDescription = 'Course Description'; const response = new HttpResponse({ - body: [new Competency(), new Competency()], + body: [{}, {}], status: 200, }); const competencyService = TestBed.inject(CompetencyService); diff --git a/src/test/javascript/spec/component/course-registration/course-prerequisites-modal/course-prerequisites-modal.component.spec.ts b/src/test/javascript/spec/component/course-registration/course-prerequisites-modal/course-prerequisites-modal.component.spec.ts index e576cae7f58a..69089d72a3f2 100644 --- a/src/test/javascript/spec/component/course-registration/course-prerequisites-modal/course-prerequisites-modal.component.spec.ts +++ b/src/test/javascript/spec/component/course-registration/course-prerequisites-modal/course-prerequisites-modal.component.spec.ts @@ -50,7 +50,7 @@ describe('CoursePrerequisitesModal', () => { it('should load prerequisites and display a card for each of them', () => { const prerequisitesOfCourseResponse: HttpResponse = new HttpResponse({ - body: [new Competency(), new Competency()], + body: [{}, {}], status: 200, }); diff --git a/src/test/javascript/spec/component/course/course-overview.component.spec.ts b/src/test/javascript/spec/component/course/course-overview.component.spec.ts index 5cc4c09f2e94..a1a024199829 100644 --- a/src/test/javascript/spec/component/course/course-overview.component.spec.ts +++ b/src/test/javascript/spec/component/course/course-overview.component.spec.ts @@ -33,7 +33,6 @@ import { By } from '@angular/platform-browser'; import { TeamAssignmentPayload } from 'app/entities/team.model'; import { Exam } from 'app/entities/exam.model'; import { CompetencyService } from 'app/course/competencies/competency.service'; -import { Competency } from 'app/entities/competency.model'; import { CourseOverviewComponent } from 'app/overview/course-overview.component'; import { BarControlConfiguration, BarControlConfigurationProvider } from 'app/shared/tab-bar/tab-bar'; import { TutorialGroupsService } from 'app/course/tutorial-groups/services/tutorial-groups.service'; @@ -101,9 +100,9 @@ const course2: Course = { exams: [exam2], description: 'Short description of course 2', shortName: 'shortName2', - competencies: [new Competency()], + competencies: [{}], tutorialGroups: [new TutorialGroup()], - prerequisites: [new Competency()], + prerequisites: [{}], numberOfCompetencies: 1, numberOfPrerequisites: 1, numberOfTutorialGroups: 1, diff --git a/src/test/javascript/spec/component/learning-paths/graph/node-details/competency-node-details.component.spec.ts b/src/test/javascript/spec/component/learning-paths/graph/node-details/competency-node-details.component.spec.ts index ec9e4eac0fba..429da0612088 100644 --- a/src/test/javascript/spec/component/learning-paths/graph/node-details/competency-node-details.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/graph/node-details/competency-node-details.component.spec.ts @@ -28,7 +28,7 @@ describe('CompetencyNodeDetailsComponent', () => { .then(() => { fixture = TestBed.createComponent(CompetencyNodeDetailsComponent); comp = fixture.componentInstance; - competency = new Competency(); + competency = {}; competency.id = 2; competency.title = 'Some arbitrary title'; competency.description = 'Some description'; diff --git a/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard-competencies.component.spec.ts b/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard-competencies.component.spec.ts index 331f6770b73a..cadb9d1e2a8b 100644 --- a/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard-competencies.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard-competencies.component.spec.ts @@ -71,7 +71,7 @@ describe('LectureWizardCompetenciesComponent', () => { }); const lectureStub = jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(of(lectureResponse)); - const competencies = [new Competency()]; + const competencies: Competency[] = [{}]; const competenciesResponse: HttpResponse = new HttpResponse({ body: competencies, status: 201, @@ -102,7 +102,7 @@ describe('LectureWizardCompetenciesComponent', () => { }); const lectureStub = jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(of(lectureResponse)); - const competencies = [new Competency()]; + const competencies: Competency[] = [{}]; const competenciesResponse: HttpResponse = new HttpResponse({ body: competencies, status: 201, @@ -128,7 +128,7 @@ describe('LectureWizardCompetenciesComponent', () => { const lectureStub = jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(throwError(() => ({ status: 404 }))); - const competencies = [new Competency()]; + const competencies: Competency[] = [{}]; const competenciesResponse: HttpResponse = new HttpResponse({ body: competencies, status: 201, @@ -153,7 +153,7 @@ describe('LectureWizardCompetenciesComponent', () => { jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(throwError(() => ({ status: 404 }))); const createStub = jest.spyOn(competencyService, 'create').mockReturnValue(throwError(() => ({ status: 404 }))); - const competencies = [new Competency()]; + const competencies: Competency[] = [{}]; const competenciesResponse: HttpResponse = new HttpResponse({ body: competencies, status: 201, @@ -182,7 +182,7 @@ describe('LectureWizardCompetenciesComponent', () => { jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(throwError(() => ({ status: 404 }))); - const competencies = [new Competency()]; + const competencies: Competency[] = [{}]; const competenciesResponse: HttpResponse = new HttpResponse({ body: competencies, status: 201, @@ -210,7 +210,7 @@ describe('LectureWizardCompetenciesComponent', () => { jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(throwError(() => ({ status: 404 }))); const editStub = jest.spyOn(competencyService, 'update').mockReturnValue(throwError(() => ({ status: 404 }))); - const competencies = [new Competency()]; + const competencies: Competency[] = [{}]; const competenciesResponse: HttpResponse = new HttpResponse({ body: competencies, status: 201, @@ -222,7 +222,7 @@ describe('LectureWizardCompetenciesComponent', () => { wizardCompetenciesComponentFixture.detectChanges(); wizardCompetenciesComponentFixture.whenStable().then(() => { - wizardCompetenciesComponent.currentlyProcessedCompetency = new Competency(); + wizardCompetenciesComponent.currentlyProcessedCompetency = {}; wizardCompetenciesComponent.editCompetency({ id: 1, title: 'Competency', @@ -240,7 +240,7 @@ describe('LectureWizardCompetenciesComponent', () => { jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(throwError(() => ({ status: 404 }))); - const competencies = [new Competency()]; + const competencies: Competency[] = [{}]; const competenciesResponse: HttpResponse = new HttpResponse({ body: competencies, status: 201, @@ -264,7 +264,7 @@ describe('LectureWizardCompetenciesComponent', () => { jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(throwError(() => ({ status: 404 }))); - const competencies = [new Competency()]; + const competencies: Competency[] = [{}]; const competenciesResponse: HttpResponse = new HttpResponse({ body: competencies, status: 201, @@ -290,7 +290,7 @@ describe('LectureWizardCompetenciesComponent', () => { wizardCompetenciesComponentFixture.detectChanges(); - const competency = new Competency(); + const competency: Competency = {}; competency.id = 12; wizardCompetenciesComponent.startEditCompetency(competency); @@ -315,7 +315,7 @@ describe('LectureWizardCompetenciesComponent', () => { wizardCompetenciesComponent.lecture.lectureUnits = [lectureUnit]; - const competency = new Competency(); + const competency: Competency = {}; competency.id = 12; competency.lectureUnits = [lectureUnit]; const result = wizardCompetenciesComponent.getConnectedUnitsForCompetency(competency); @@ -332,7 +332,7 @@ describe('LectureWizardCompetenciesComponent', () => { wizardCompetenciesComponentFixture.detectChanges(); - const competency = new Competency(); + const competency: Competency = {}; competency.id = 12; const result = wizardCompetenciesComponent.getConnectedUnitsForCompetency(competency); @@ -347,7 +347,7 @@ describe('LectureWizardCompetenciesComponent', () => { jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(throwError(() => ({ status: 404 }))); jest.spyOn(competencyService, 'getAllForCourse').mockReturnValue(throwError(() => ({ status: 404 }))); - const createStub = jest.spyOn(competencyService, 'create').mockReturnValue(of(new HttpResponse({ status: 201, body: new Competency() }))); + const createStub = jest.spyOn(competencyService, 'create').mockReturnValue(of(new HttpResponse({ status: 201, body: {} }))); const alertStub = jest.spyOn(alertService, 'success'); wizardCompetenciesComponentFixture.detectChanges(); @@ -377,7 +377,7 @@ describe('LectureWizardCompetenciesComponent', () => { jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(throwError(() => ({ status: 404 }))); jest.spyOn(competencyService, 'getAllForCourse').mockReturnValue(throwError(() => ({ status: 404 }))); - const competency = new Competency(); + const competency: Competency = {}; const exercise = new TextExercise(undefined, undefined); exercise.id = 2; competency.exercises = [exercise]; @@ -422,7 +422,7 @@ describe('LectureWizardCompetenciesComponent', () => { jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(throwError(() => ({ status: 404 }))); jest.spyOn(competencyService, 'getAllForCourse').mockReturnValue(throwError(() => ({ status: 404 }))); - const createStub = jest.spyOn(competencyService, 'create').mockReturnValue(of(new HttpResponse({ status: 201, body: new Competency() }))); + const createStub = jest.spyOn(competencyService, 'create').mockReturnValue(of(new HttpResponse({ status: 201, body: {} }))); const alertStub = jest.spyOn(alertService, 'success'); wizardCompetenciesComponentFixture.detectChanges(); @@ -447,7 +447,7 @@ describe('LectureWizardCompetenciesComponent', () => { jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(throwError(() => ({ status: 404 }))); jest.spyOn(competencyService, 'getAllForCourse').mockReturnValue(throwError(() => ({ status: 404 }))); - const editStub = jest.spyOn(competencyService, 'update').mockReturnValue(of(new HttpResponse({ status: 201, body: new Competency() }))); + const editStub = jest.spyOn(competencyService, 'update').mockReturnValue(of(new HttpResponse({ status: 201, body: {} }))); const alertStub = jest.spyOn(alertService, 'success'); wizardCompetenciesComponentFixture.detectChanges(); @@ -458,7 +458,7 @@ describe('LectureWizardCompetenciesComponent', () => { }; wizardCompetenciesComponentFixture.whenStable().then(() => { - wizardCompetenciesComponent.currentlyProcessedCompetency = new Competency(); + wizardCompetenciesComponent.currentlyProcessedCompetency = {}; wizardCompetenciesComponent.isEditingCompetency = true; wizardCompetenciesComponent.onCompetencyFormSubmitted(formData); @@ -478,7 +478,7 @@ describe('LectureWizardCompetenciesComponent', () => { jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(throwError(() => ({ status: 404 }))); jest.spyOn(competencyService, 'getAllForCourse').mockReturnValue(throwError(() => ({ status: 404 }))); - const competency = new Competency(); + const competency: Competency = {}; const exercise = new TextExercise(undefined, undefined); exercise.id = 2; competency.exercises = [exercise]; @@ -500,7 +500,7 @@ describe('LectureWizardCompetenciesComponent', () => { }; wizardCompetenciesComponentFixture.whenStable().then(() => { - wizardCompetenciesComponent.currentlyProcessedCompetency = new Competency(); + wizardCompetenciesComponent.currentlyProcessedCompetency = {}; wizardCompetenciesComponent.lecture.lectureUnits = [unit]; wizardCompetenciesComponent.isEditingCompetency = true; wizardCompetenciesComponent.onCompetencyFormSubmitted(formData); From 1d375668d69455d34e15850ed697e5fb567a24ef Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Tue, 28 May 2024 15:01:50 +0200 Subject: [PATCH 12/78] Adjust client code to support new prerequisites --- .../competency/PrerequisiteResponseDTO.java | 23 ++-- .../competency-card.component.html | 8 +- .../competency-management.component.html | 13 ++- .../competency-management.component.ts | 74 ++++-------- .../course/competencies/competency.service.ts | 1 + .../competencies/prerequisite.service.ts | 109 ++++++++++++++++++ .../webapp/app/entities/competency.model.ts | 1 - src/main/webapp/app/entities/course.model.ts | 4 +- .../webapp/app/entities/prerequisite.model.ts | 22 +++- .../course-competencies.component.html | 2 +- .../course-competencies.component.ts | 6 +- .../course-prerequisites-modal.component.ts | 8 +- 12 files changed, 193 insertions(+), 78 deletions(-) create mode 100644 src/main/webapp/app/course/competencies/prerequisite.service.ts diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteResponseDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteResponseDTO.java index 30ac2397cddc..e504fa22cb19 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteResponseDTO.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteResponseDTO.java @@ -12,20 +12,27 @@ */ @JsonInclude(JsonInclude.Include.NON_EMPTY) public record PrerequisiteResponseDTO(long id, String title, String description, CompetencyTaxonomy taxonomy, ZonedDateTime softDueDate, int masteryThreshold, boolean optional, - SourceCourseDTO sourceCourseDTO) { - + LinkedCourseCompetencyDTO linkedCourseCompetencyDTO) { + + /** + * Converts a Prerequisite to a PrerequisiteResponseDTO (and its linked CourseCompetency to a LinkedCourseCompetencyDTO) + * + * @param prerequisite the Prerequisite to convert + * @return the PrerequisiteResponseDTO + */ public static PrerequisiteResponseDTO of(Prerequisite prerequisite) { - SourceCourseDTO sourceCourseDTO = null; - if (prerequisite.getLinkedCourseCompetency() != null && prerequisite.getLinkedCourseCompetency().getCourse() != null) { - var course = prerequisite.getLinkedCourseCompetency().getCourse(); - sourceCourseDTO = new SourceCourseDTO(course.getId(), course.getTitle()); + LinkedCourseCompetencyDTO linkedCourseCompetencyDTO = null; + var linkedCompetency = prerequisite.getLinkedCourseCompetency(); + if (linkedCompetency != null && linkedCompetency.getCourse() != null) { + var course = linkedCompetency.getCourse(); + linkedCourseCompetencyDTO = new LinkedCourseCompetencyDTO(linkedCompetency.getId(), course.getId(), course.getTitle(), course.getSemester()); } return new PrerequisiteResponseDTO(prerequisite.getId(), prerequisite.getTitle(), prerequisite.getDescription(), prerequisite.getTaxonomy(), prerequisite.getSoftDueDate(), - prerequisite.getMasteryThreshold(), prerequisite.isOptional(), sourceCourseDTO); + prerequisite.getMasteryThreshold(), prerequisite.isOptional(), linkedCourseCompetencyDTO); } - private record SourceCourseDTO(long id, String title) { + private record LinkedCourseCompetencyDTO(long id, long courseId, String courseTitle, String semester) { } } diff --git a/src/main/webapp/app/course/competencies/competency-card/competency-card.component.html b/src/main/webapp/app/course/competencies/competency-card/competency-card.component.html index da6640b789d4..4f4f2349042e 100644 --- a/src/main/webapp/app/course/competencies/competency-card/competency-card.component.html +++ b/src/main/webapp/app/course/competencies/competency-card/competency-card.component.html @@ -38,10 +38,10 @@

@if (competency.description) {

} - @if (isPrerequisite && competency.course) { + @if (isPrerequisite && competency.linkedCourseCompetency?.course) {
- {{ competency.course.title }} - {{ competency.course.semester }} + {{ competency.linkedCourseCompetency.course.title }} + {{ competency.linkedCourseCompetency.course.semester }}
} @@ -50,7 +50,7 @@

{{ 'artemisApp.competency.competencyCard.softDueDate' | artemisTranslate }} - {{ competency.softDueDate | artemisTimeAgo }} + {{ competency.softDueDate! | artemisTimeAgo }}
} 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 a0964bfcb59d..5f7cb8ccb5e2 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 @@ -141,12 +141,10 @@

{{ 'artemisApp.competency.competencies' | artemisTranslate }}

- +
+
@if (prerequisites.length) { @@ -183,13 +181,16 @@

@@ -201,7 +209,7 @@

- @if (prerequisite.course) { + @if (prerequisite.linkedCourseCompetency?.course) { } + -
+
-

+

@@ -181,19 +184,24 @@

- @if (prerequisite.linkedCourseCompetency?.course) { + @if (prerequisite.linkedCourseCompetency?.course?.id) { } - -
} @else { - {{ 'artemisApp.competency.prerequisite.empty' | artemisTranslate }} + }
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 8efd80d999fa..41ae792d68f0 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 @@ -12,7 +12,7 @@ import { getIcon, } from 'app/entities/competency.model'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; -import { filter, finalize, map, switchMap } from 'rxjs/operators'; +import { filter, map, switchMap } from 'rxjs/operators'; import { onError } from 'app/shared/util/global.utils'; import { Subject, Subscription, forkJoin } from 'rxjs'; import { faFileImport, faPencilAlt, faPlus, faRobot, faTrash } from '@fortawesome/free-solid-svg-icons'; @@ -104,15 +104,16 @@ export class CompetencyManagementComponent implements OnInit, OnDestroy { } /** - * Remove a prerequisite from the course + * Deletes a prerequisite from the course * * @param prerequisiteId the id of the prerequisite */ - removePrerequisite(prerequisiteId: number) { - this.prerequisiteService.removePrerequisite(prerequisiteId, this.courseId).subscribe({ + deletePrerequisite(prerequisiteId: number) { + this.prerequisiteService.deletePrerequisite(prerequisiteId, this.courseId).subscribe({ next: () => { - const index = this.prerequisites.findIndex((prerequisite) => prerequisite.id === prerequisiteId); - this.prerequisites.splice(index, 1); + this.alertService.success('artemisApp.prerequisite.manage.deleted'); + this.prerequisites = this.prerequisites.filter((prerequisite) => prerequisite.id !== prerequisiteId); + this.dialogErrorSource.next(''); }, error: (error: HttpErrorResponse) => onError(this.alertService, error), }); @@ -139,43 +140,32 @@ export class CompetencyManagementComponent implements OnInit, OnDestroy { */ loadData() { this.isLoading = true; - this.prerequisiteService.getAllPrerequisitesForCourse(this.courseId).subscribe({ - next: (prerequisites) => { + const relationsObservable = this.competencyService.getCompetencyRelations(this.courseId); + const prerequisitesObservable = this.prerequisiteService.getAllPrerequisitesForCourse(this.courseId); + const competencyProgressObservable = this.competencyService.getAllForCourse(this.courseId).pipe( + switchMap((res) => { + this.competencies = res.body!; + + const progressObservable = this.competencies.map((lg) => { + return this.competencyService.getCourseProgress(lg.id!, this.courseId); + }); + + return forkJoin(progressObservable); + }), + ); + forkJoin([relationsObservable, prerequisitesObservable, competencyProgressObservable]).subscribe({ + next: ([competencyRelations, prerequisites, competencyProgressResponses]) => { this.prerequisites = prerequisites; + this.relations = (competencyRelations.body ?? []).map((relationDTO) => dtoToCompetencyRelation(relationDTO)); + + for (const competencyProgressResponse of competencyProgressResponses) { + const courseCompetencyProgress: CourseCompetencyProgress = competencyProgressResponse.body!; + this.competencies.find((competency) => competency.id === courseCompetencyProgress.competencyId)!.courseProgress = courseCompetencyProgress; + } + this.isLoading = false; }, error: (errorResponse: HttpErrorResponse) => onError(this.alertService, errorResponse), }); - this.competencyService - .getAllForCourse(this.courseId) - .pipe( - switchMap((res) => { - this.competencies = res.body!; - - const relationsObservable = this.competencyService.getCompetencyRelations(this.courseId); - - const progressObservable = this.competencies.map((lg) => { - return this.competencyService.getCourseProgress(lg.id!, this.courseId); - }); - - return forkJoin([relationsObservable, forkJoin(progressObservable)]); - }), - ) - .pipe( - finalize(() => { - this.isLoading = false; - }), - ) - .subscribe({ - next: ([competencyRelations, competencyProgressResponses]) => { - this.relations = (competencyRelations.body ?? []).map((relationDTO) => dtoToCompetencyRelation(relationDTO)); - - for (const competencyProgressResponse of competencyProgressResponses) { - const courseCompetencyProgress: CourseCompetencyProgress = competencyProgressResponse.body!; - this.competencies.find((competency) => competency.id === courseCompetencyProgress.competencyId)!.courseProgress = courseCompetencyProgress; - } - }, - error: (errorResponse: HttpErrorResponse) => onError(this.alertService, errorResponse), - }); } /** diff --git a/src/main/webapp/app/course/competencies/competency-management/prerequisite-import.component.ts b/src/main/webapp/app/course/competencies/competency-management/prerequisite-import.component.ts deleted file mode 100644 index fc21f978bd6b..000000000000 --- a/src/main/webapp/app/course/competencies/competency-management/prerequisite-import.component.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Component } from '@angular/core'; -import { Router } from '@angular/router'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { CompetencyPagingService } from 'app/course/competencies/competency-paging.service'; -import { Competency } from 'app/entities/competency.model'; -import { Column, ImportComponent } from 'app/shared/import/import.component'; -import { SortService } from 'app/shared/service/sort.service'; - -const tableColumns: Column[] = [ - { - name: 'TITLE', - getProperty(entity: Competency) { - return entity.title; - }, - }, - { - name: 'COURSE_TITLE', - getProperty(entity: Competency) { - return entity.course?.title; - }, - }, - { - name: 'SEMESTER', - getProperty(entity: Competency) { - return entity.course?.semester; - }, - }, -]; - -@Component({ - selector: 'jhi-prerequisite-import', - templateUrl: '../../../shared/import/import.component.html', -}) -export class PrerequisiteImportComponent extends ImportComponent { - constructor(router: Router, sortService: SortService, activeModal: NgbActiveModal, pagingService: CompetencyPagingService) { - super(router, sortService, activeModal, pagingService); - this.columns = tableColumns; - this.entityName = 'competency.prerequisite'; - } -} diff --git a/src/main/webapp/app/course/competencies/competency.module.ts b/src/main/webapp/app/course/competencies/competency.module.ts index 7f3f10860f32..1d8f9891b3be 100644 --- a/src/main/webapp/app/course/competencies/competency.module.ts +++ b/src/main/webapp/app/course/competencies/competency.module.ts @@ -9,7 +9,6 @@ import { EditCompetencyComponent } from './edit-competency/edit-competency.compo import { CompetencyManagementComponent } from './competency-management/competency-management.component'; import { CompetencyCardComponent } from 'app/course/competencies/competency-card/competency-card.component'; import { CompetenciesPopoverComponent } from './competencies-popover/competencies-popover.component'; -import { PrerequisiteImportComponent } from 'app/course/competencies/competency-management/prerequisite-import.component'; import { NgxGraphModule } from '@swimlane/ngx-graph'; import { CompetencyRingsComponent } from 'app/course/competencies/competency-rings/competency-rings.component'; import { FormDateTimePickerModule } from 'app/shared/date-time-picker/date-time-picker.module'; @@ -20,13 +19,14 @@ import { CourseDescriptionFormComponent } from 'app/course/competencies/generate import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; import { IrisModule } from 'app/iris/iris.module'; import { CompetencyImportCourseComponent } from 'app/course/competencies/competency-management/competency-import-course.component'; -import { ImportCompetenciesComponent } from 'app/course/competencies/import-competencies/import-competencies.component'; import { CompetencySearchComponent } from 'app/course/competencies/import-competencies/competency-search.component'; import { ImportCompetenciesTableComponent } from 'app/course/competencies/import-competencies/import-competencies-table.component'; import { TaxonomySelectComponent } from 'app/course/competencies/taxonomy-select/taxonomy-select.component'; import { CompetencyRelationGraphComponent } from 'app/course/competencies/competency-management/competency-relation-graph.component'; import { CourseImportStandardizedCompetenciesComponent } from 'app/course/competencies/import-standardized-competencies/course-import-standardized-competencies.component'; import { ArtemisStandardizedCompetencyModule } from 'app/shared/standardized-competencies/standardized-competency.module'; +import { ImportCompetenciesComponent } from 'app/course/competencies/import-competencies/import-competencies.component'; +import { ImportPrerequisitesComponent } from 'app/course/competencies/import-competencies/import-prerequisites.component'; @NgModule({ imports: [ @@ -47,7 +47,6 @@ import { ArtemisStandardizedCompetencyModule } from 'app/shared/standardized-com CompetencyRingsComponent, CreateCompetencyComponent, EditCompetencyComponent, - ImportCompetenciesComponent, CompetencySearchComponent, GenerateCompetenciesComponent, CompetencyRecommendationDetailComponent, @@ -55,12 +54,13 @@ import { ArtemisStandardizedCompetencyModule } from 'app/shared/standardized-com CompetencyManagementComponent, CompetencyCardComponent, CompetenciesPopoverComponent, - PrerequisiteImportComponent, CompetencyImportCourseComponent, ImportCompetenciesTableComponent, TaxonomySelectComponent, CompetencyRelationGraphComponent, CourseImportStandardizedCompetenciesComponent, + ImportCompetenciesComponent, + ImportPrerequisitesComponent, ], exports: [CompetencyCardComponent, CompetenciesPopoverComponent, CompetencyFormComponent, CompetencyRingsComponent, TaxonomySelectComponent], }) diff --git a/src/main/webapp/app/course/competencies/import-competencies/competency-search.component.ts b/src/main/webapp/app/course/competencies/import-competencies/competency-search.component.ts index da700fa93030..24b924a45c37 100644 --- a/src/main/webapp/app/course/competencies/import-competencies/competency-search.component.ts +++ b/src/main/webapp/app/course/competencies/import-competencies/competency-search.component.ts @@ -2,15 +2,15 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'; import { ButtonType } from 'app/shared/components/button.component'; import { getSemesters } from 'app/utils/semester-utils'; -import { CompetencyFilter } from 'app/shared/table/pageable-table'; +import { CourseCompetencyFilter } from 'app/shared/table/pageable-table'; @Component({ selector: 'jhi-competency-search', templateUrl: './competency-search.component.html', }) export class CompetencySearchComponent { - @Input() search: CompetencyFilter; - @Output() searchChange = new EventEmitter(); + @Input() search: CourseCompetencyFilter; + @Output() searchChange = new EventEmitter(); advancedSearchEnabled = false; diff --git a/src/main/webapp/app/course/competencies/import-competencies/import-competencies.component.html b/src/main/webapp/app/course/competencies/import-competencies/import-competencies.component.html deleted file mode 100644 index ab1f0ac39ad8..000000000000 --- a/src/main/webapp/app/course/competencies/import-competencies/import-competencies.component.html +++ /dev/null @@ -1,34 +0,0 @@ -
-

{{ 'artemisApp.competency.import.title' | artemisTranslate }}

- -

{{ 'artemisApp.competency.import.searchTableHeader' | artemisTranslate }}

- @if (searchedCompetencies.resultsOnPage?.length) { - - - - - - } @else { - {{ 'artemisApp.competency.import.searchTableEmpty' | artemisTranslate }} - } -

{{ 'artemisApp.competency.import.selectedTableHeader' | artemisTranslate }}

- @if (selectedCompetencies.resultsOnPage?.length) { - - - - - - } @else { - {{ 'artemisApp.competency.import.selectedTableEmpty' | artemisTranslate }} - } -
-
- {{ 'artemisApp.competency.import.importRelations' | artemisTranslate }} - -
-
- - -
-
-
diff --git a/src/main/webapp/app/course/competencies/import-competencies/import-competencies.component.ts b/src/main/webapp/app/course/competencies/import-competencies/import-competencies.component.ts index 8d8901a2e33f..61076d784c9f 100644 --- a/src/main/webapp/app/course/competencies/import-competencies/import-competencies.component.ts +++ b/src/main/webapp/app/course/competencies/import-competencies/import-competencies.component.ts @@ -1,215 +1,24 @@ -import { Component, HostListener, OnInit } from '@angular/core'; -import { faBan, faFileImport, faSave, faTrash } from '@fortawesome/free-solid-svg-icons'; -import { ButtonType } from 'app/shared/components/button.component'; -import { ActivatedRoute, Router } from '@angular/router'; -import { ComponentCanDeactivate } from 'app/shared/guard/can-deactivate.model'; -import { TranslateService } from '@ngx-translate/core'; -import { CompetencyService } from 'app/course/competencies/competency.service'; import { HttpErrorResponse } from '@angular/common/http'; +import { Component } from '@angular/core'; +import { ImportCourseCompetenciesComponent } from 'app/course/competencies/import-competencies/import-course-competencies.component'; import { onError } from 'app/shared/util/global.utils'; -import { AlertService } from 'app/core/util/alert.service'; -import { Competency } from 'app/entities/competency.model'; -import { CompetencyFilter, PageableSearch, SearchResult, SortingOrder } from 'app/shared/table/pageable-table'; -import { SortService } from 'app/shared/service/sort.service'; @Component({ selector: 'jhi-import-competencies', - templateUrl: './import-competencies.component.html', + templateUrl: './import-course-competencies.component.html', }) -export class ImportCompetenciesComponent implements OnInit, ComponentCanDeactivate { - courseId: number; - isLoading = false; - isSubmitted = false; - importRelations = true; - showAdvancedSearch = false; - disabledIds: number[] = []; - searchedCompetencies: SearchResult = { resultsOnPage: [], numberOfPages: 0 }; - selectedCompetencies: SearchResult = { resultsOnPage: [], numberOfPages: 0 }; +export class ImportCompetenciesComponent extends ImportCourseCompetenciesComponent { + entityType = 'competency'; + allowRelationImport = true; - //filter and search objects for the competency search. - filter: CompetencyFilter = { - courseTitle: '', - description: '', - semester: '', - title: '', - }; - search: PageableSearch = { - page: 1, - pageSize: 10, - sortingOrder: SortingOrder.DESCENDING, - sortedColumn: 'ID', - }; - - //search object for the selected competencies. As we don't want pagination page and pageSize are 0 - selectedCompetenciesSearch: PageableSearch = { - page: 0, - pageSize: 0, - sortingOrder: SortingOrder.DESCENDING, - sortedColumn: 'ID', - }; - - //Icons - protected readonly faBan = faBan; - protected readonly faSave = faSave; - protected readonly faFileImport = faFileImport; - protected readonly faTrash = faTrash; - //Other constants - protected readonly ButtonType = ButtonType; - //used for sorting of the selected competencies - protected readonly columnMapping = { - ID: 'id', - TITLE: 'title', - DESCRIPTION: 'description', - COURSE_TITLE: 'course.title', - SEMESTER: 'course.semester', - }; - - constructor( - private activatedRoute: ActivatedRoute, - private router: Router, - private translateService: TranslateService, - private competencyService: CompetencyService, - private alertService: AlertService, - private sortingService: SortService, - ) {} - - ngOnInit(): void { - this.activatedRoute.params.subscribe((params) => { - this.courseId = Number(params['courseId']); - this.performSearch(); - //load competencies of this course to disable their import buttons - this.competencyService.getAllForCourse(this.courseId).subscribe({ - next: (res) => { - if (res.body) { - this.disabledIds = res.body.map((competency) => competency.id).filter((id): id is number => !!id); - } - }, - error: (error: HttpErrorResponse) => onError(this.alertService, error), - }); - }); - } - - /** - * Callback that updates the filter for the competency search and fetches new data from the server. - * - * @param filter the new filter - */ - filterChange(filter: CompetencyFilter) { - this.filter = filter; - //navigate back to the first page when the filter changes - this.search.page = 1; - this.performSearch(); - } - - /** - * Callback that updates the pagination/sorting for the competency search and fetches new data from the server. - * - * @param search the new pagination/sorting - */ - searchChange(search: PageableSearch) { - this.search = search; - this.performSearch(); - } - - /** - * Fetches a page of competencies matching a PageableSearch from the server. - * - */ - performSearch() { - this.isLoading = true; - this.competencyService.getForImport({ ...this.filter, ...this.search }).subscribe({ - next: (res) => { - this.searchedCompetencies = res; - this.isLoading = false; - }, - error: (error: HttpErrorResponse) => onError(this.alertService, error), - }); - } - - /** - * Callback that sorts the selected competencies - * - * @param search the PageableSearch object with the updated sorting data - */ - sortSelected(search: PageableSearch) { - this.selectedCompetencies.resultsOnPage = this.sortingService.sortByProperty( - this.selectedCompetencies.resultsOnPage, - this.columnMapping[search.sortedColumn], - search.sortingOrder === SortingOrder.ASCENDING, - ); - } - - /** - * Callback to add a competency to the selected list - * - * @param competency the competency to add - */ - selectCompetency(competency: Competency) { - if (competency.id) { - this.disabledIds.push(competency.id); - } - this.selectedCompetencies.resultsOnPage.push(competency); - this.sortSelected(this.selectedCompetenciesSearch); - } - - /** - * Callback to remove a competency from the selected list - * - * @param competency the competency to remove - */ - removeCompetency(competency: Competency) { - if (competency.id) { - this.disabledIds = this.disabledIds.filter((id) => id !== competency.id); - } - this.selectedCompetencies.resultsOnPage = this.selectedCompetencies.resultsOnPage.filter((c) => c.id !== competency.id); - } - - /** - * Only allows submitting if at least one competency has been selected for import - */ - isSubmitPossible() { - return this.selectedCompetencies.resultsOnPage.length > 0; - } - - /** - * Submits the competencies to import and if successful, navigates back - */ onSubmit() { - this.competencyService.importBulk(this.selectedCompetencies.resultsOnPage, this.courseId, this.importRelations).subscribe({ + this.competencyService.importBulk(this.selectedCourseCompetencies.resultsOnPage, this.courseId, this.importRelations).subscribe({ next: (res) => { - this.alertService.success('artemisApp.competency.import.success', { noOfCompetencies: res.body?.length ?? 0 }); + this.alertService.success('artemisApp.competency.import.success', { numCompetencies: res.body?.length ?? 0 }); this.isSubmitted = true; this.router.navigate(['../'], { relativeTo: this.activatedRoute }); }, error: (error: HttpErrorResponse) => onError(this.alertService, error), }); } - - /** - * Cancels the import and navigates back - */ - onCancel() { - this.router.navigate(['../'], { relativeTo: this.activatedRoute }); - } - - /** - * Only allow to leave page after submitting or if no pending changes exist - */ - canDeactivate() { - return this.isSubmitted || (!this.isLoading && this.selectedCompetencies.resultsOnPage.length === 0); - } - - get canDeactivateWarning(): string { - return this.translateService.instant('pendingChanges'); - } - - /** - * Displays the alert for confirming refreshing or closing the page if there are unsaved changes - */ - @HostListener('window:beforeunload', ['$event']) - unloadNotification(event: any) { - if (!this.canDeactivate()) { - event.returnValue = this.canDeactivateWarning; - } - } } diff --git a/src/main/webapp/app/course/competencies/import-competencies/import-course-competencies.component.html b/src/main/webapp/app/course/competencies/import-competencies/import-course-competencies.component.html new file mode 100644 index 000000000000..c8bca019d304 --- /dev/null +++ b/src/main/webapp/app/course/competencies/import-competencies/import-course-competencies.component.html @@ -0,0 +1,41 @@ +
+

+ +

+ @if (searchedCourseCompetencies.resultsOnPage?.length) { + + + + + + } @else { + + } +

+ @if (selectedCourseCompetencies.resultsOnPage?.length) { + + + + + + } @else { + + } +
+ @if (allowRelationImport) { +
+ + +
+ } +
+ + +
+
+
diff --git a/src/main/webapp/app/course/competencies/import-competencies/import-course-competencies.component.ts b/src/main/webapp/app/course/competencies/import-competencies/import-course-competencies.component.ts new file mode 100644 index 000000000000..534f0deaa0cc --- /dev/null +++ b/src/main/webapp/app/course/competencies/import-competencies/import-course-competencies.component.ts @@ -0,0 +1,210 @@ +import { ComponentCanDeactivate } from 'app/shared/guard/can-deactivate.model'; +import { CourseCompetencyFilter, PageableSearch, SearchResult, SortingOrder } from 'app/shared/table/pageable-table'; +import { CourseCompetency } from 'app/entities/competency.model'; +import { CompetencyService } from 'app/course/competencies/competency.service'; +import { AlertService } from 'app/core/util/alert.service'; +import { SortService } from 'app/shared/service/sort.service'; +import { onError } from 'app/shared/util/global.utils'; +import { Component, HostListener, OnInit, inject } from '@angular/core'; +import { faBan, faFileImport, faSave, faTrash } from '@fortawesome/free-solid-svg-icons'; +import { ButtonType } from 'app/shared/components/button.component'; +import { ActivatedRoute, Router } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; +import { HttpErrorResponse } from '@angular/common/http'; + +/** + * An abstract component used to import course competencies. Its concrete implementations are + * {@link ImportCompetenciesComponent} and {@link ImportPrerequisitesComponent} + */ +@Component({ template: '' }) +export abstract class ImportCourseCompetenciesComponent implements OnInit, ComponentCanDeactivate { + // this attribute has to be set when using the common template (import-course-competencies.component.html) + abstract entityType: string; + // set this attribute to hide the options to import relation + allowRelationImport: boolean = false; + + courseId: number; + isLoading = false; + isSubmitted = false; + importRelations = true; + showAdvancedSearch = false; + disabledIds: number[] = []; + searchedCourseCompetencies: SearchResult = { resultsOnPage: [], numberOfPages: 0 }; + selectedCourseCompetencies: SearchResult = { resultsOnPage: [], numberOfPages: 0 }; + + //filter and search objects for the course competency search. + filter: CourseCompetencyFilter = { + courseTitle: '', + description: '', + semester: '', + title: '', + }; + search: PageableSearch = { + page: 1, + pageSize: 10, + sortingOrder: SortingOrder.DESCENDING, + sortedColumn: 'ID', + }; + + //search object for the selected course competencies. As we don't want pagination page and pageSize are 0 + selectedCourseCompetenciesSearch: PageableSearch = { + page: 0, + pageSize: 0, + sortingOrder: SortingOrder.DESCENDING, + sortedColumn: 'ID', + }; + + //Icons + protected readonly faBan = faBan; + protected readonly faSave = faSave; + protected readonly faFileImport = faFileImport; + protected readonly faTrash = faTrash; + //Other constants + protected readonly ButtonType = ButtonType; + //used for sorting of the selected course competencies + protected readonly columnMapping = { + ID: 'id', + TITLE: 'title', + DESCRIPTION: 'description', + COURSE_TITLE: 'course.title', + SEMESTER: 'course.semester', + }; + + protected readonly activatedRoute: ActivatedRoute = inject(ActivatedRoute); + protected readonly router: Router = inject(Router); + protected readonly competencyService: CompetencyService = inject(CompetencyService); + protected readonly alertService: AlertService = inject(AlertService); + private readonly translateService: TranslateService = inject(TranslateService); + private readonly sortingService: SortService = inject(SortService); + + ngOnInit(): void { + this.activatedRoute.params.subscribe((params) => { + this.courseId = Number(params['courseId']); + this.performSearch(); + //load competencies of this course to disable their import buttons + this.competencyService.getAllForCourse(this.courseId).subscribe({ + next: (res) => { + if (res.body) { + this.disabledIds = res.body.map((competency) => competency.id).filter((id): id is number => !!id); + } + }, + error: (error: HttpErrorResponse) => onError(this.alertService, error), + }); + }); + } + + /** + * Submits the course competencies to import and if successful, should navigate back + */ + abstract onSubmit(): void; + + /** + * Callback that updates the filter for the competency search and fetches new data from the server. + * + * @param filter the new filter + */ + filterChange(filter: CourseCompetencyFilter) { + this.filter = filter; + //navigate back to the first page when the filter changes + this.search.page = 1; + this.performSearch(); + } + + /** + * Callback that updates the pagination/sorting for the competency search and fetches new data from the server. + * + * @param search the new pagination/sorting + */ + searchChange(search: PageableSearch) { + this.search = search; + this.performSearch(); + } + + /** + * Fetches a page of course competencies matching a PageableSearch from the server. + * + */ + performSearch() { + this.isLoading = true; + this.competencyService.getForImport({ ...this.filter, ...this.search }).subscribe({ + next: (res) => { + this.searchedCourseCompetencies = res; + this.isLoading = false; + }, + error: (error: HttpErrorResponse) => onError(this.alertService, error), + }); + } + + /** + * Callback that sorts the selected course competencies + * + * @param search the PageableSearch object with the updated sorting data + */ + sortSelected(search: PageableSearch) { + this.selectedCourseCompetencies.resultsOnPage = this.sortingService.sortByProperty( + this.selectedCourseCompetencies.resultsOnPage, + this.columnMapping[search.sortedColumn], + search.sortingOrder === SortingOrder.ASCENDING, + ); + } + + /** + * Callback to add a course competency to the selected list + * + * @param competency the competency to add + */ + selectCompetency(competency: CourseCompetency) { + if (competency.id) { + this.disabledIds.push(competency.id); + } + this.selectedCourseCompetencies.resultsOnPage.push(competency); + this.sortSelected(this.selectedCourseCompetenciesSearch); + } + + /** + * Callback to remove a course competency from the selected list + * + * @param competency the competency to remove + */ + removeCompetency(competency: CourseCompetency) { + if (competency.id) { + this.disabledIds = this.disabledIds.filter((id) => id !== competency.id); + } + this.selectedCourseCompetencies.resultsOnPage = this.selectedCourseCompetencies.resultsOnPage.filter((c) => c.id !== competency.id); + } + + /** + * Only allows submitting if at least one competency has been selected for import + */ + isSubmitPossible() { + return this.selectedCourseCompetencies.resultsOnPage.length > 0; + } + + /** + * Cancels the import and navigates back + */ + onCancel() { + this.router.navigate(['../'], { relativeTo: this.activatedRoute }); + } + + /** + * Only allow to leave page after submitting or if no pending changes exist + */ + canDeactivate() { + return this.isSubmitted || (!this.isLoading && this.selectedCourseCompetencies.resultsOnPage.length === 0); + } + + get canDeactivateWarning(): string { + return this.translateService.instant('pendingChanges'); + } + + /** + * Displays the alert for confirming refreshing or closing the page if there are unsaved changes + */ + @HostListener('window:beforeunload', ['$event']) + unloadNotification(event: any) { + if (!this.canDeactivate()) { + event.returnValue = this.canDeactivateWarning; + } + } +} diff --git a/src/main/webapp/app/course/competencies/import-competencies/import-prerequisites.component.ts b/src/main/webapp/app/course/competencies/import-competencies/import-prerequisites.component.ts new file mode 100644 index 000000000000..afaa2421b716 --- /dev/null +++ b/src/main/webapp/app/course/competencies/import-competencies/import-prerequisites.component.ts @@ -0,0 +1,28 @@ +import { HttpErrorResponse } from '@angular/common/http'; +import { Component, inject } from '@angular/core'; +import { ImportCourseCompetenciesComponent } from 'app/course/competencies/import-competencies/import-course-competencies.component'; +import { onError } from 'app/shared/util/global.utils'; +import { PrerequisiteService } from '../prerequisite.service'; + +@Component({ + selector: 'jhi-import-prerequisites', + templateUrl: './import-course-competencies.component.html', +}) +export class ImportPrerequisitesComponent extends ImportCourseCompetenciesComponent { + entityType = 'prerequisite'; + allowRelationImport = false; + + private readonly prerequisiteService: PrerequisiteService = inject(PrerequisiteService); + + onSubmit() { + const idsToImport = this.selectedCourseCompetencies.resultsOnPage.map((c) => c.id).filter((c): c is number => c !== undefined); + this.prerequisiteService.importPrerequisites(idsToImport, this.courseId).subscribe({ + next: (res) => { + this.alertService.success('artemisApp.prerequisite.import.success', { numPrerequisites: res.length }); + this.isSubmitted = true; + this.router.navigate(['../'], { relativeTo: this.activatedRoute }); + }, + error: (error: HttpErrorResponse) => onError(this.alertService, error), + }); + } +} diff --git a/src/main/webapp/app/course/competencies/prerequisite.service.ts b/src/main/webapp/app/course/competencies/prerequisite.service.ts index dcf7c5f5796d..521c5646c2c0 100644 --- a/src/main/webapp/app/course/competencies/prerequisite.service.ts +++ b/src/main/webapp/app/course/competencies/prerequisite.service.ts @@ -39,7 +39,7 @@ export class PrerequisiteService { .pipe(map((resp) => (resp.body ? this.convertResponseDTOToPrerequisite(resp.body) : undefined))); } - removePrerequisite(prerequisiteId: number, courseId: number) { + deletePrerequisite(prerequisiteId: number, courseId: number) { return this.httpClient.delete(`${this.resourceURL}/courses/${courseId}/competencies/prerequisites/${prerequisiteId}`, { observe: 'response' }); } diff --git a/src/main/webapp/app/course/manage/course-management.route.ts b/src/main/webapp/app/course/manage/course-management.route.ts index e1aa1c944682..d267c090bb3b 100644 --- a/src/main/webapp/app/course/manage/course-management.route.ts +++ b/src/main/webapp/app/course/manage/course-management.route.ts @@ -29,6 +29,7 @@ import { ImportCompetenciesComponent } from 'app/course/competencies/import-comp import { LocalCIGuard } from 'app/localci/localci-guard.service'; import { IrisGuard } from 'app/iris/iris-guard.service'; import { CourseImportStandardizedCompetenciesComponent } from 'app/course/competencies/import-standardized-competencies/course-import-standardized-competencies.component'; +import { ImportPrerequisitesComponent } from 'app/course/competencies/import-competencies/import-prerequisites.component'; export const courseManagementState: Routes = [ { @@ -259,6 +260,16 @@ export const courseManagementState: Routes = [ canActivate: [UserRouteAccessService, IrisGuard], canDeactivate: [PendingChangesGuard], }, + { + path: 'import-prerequisites', + component: ImportPrerequisitesComponent, + data: { + authorities: [Authority.INSTRUCTOR, Authority.ADMIN], + pageTitle: 'artemisApp.prerequisite.import.title', + }, + canActivate: [UserRouteAccessService], + canDeactivate: [PendingChangesGuard], + }, ], }, { diff --git a/src/main/webapp/app/overview/course-competencies/course-competencies.component.html b/src/main/webapp/app/overview/course-competencies/course-competencies.component.html index 8bd003867208..5fb79ef44421 100644 --- a/src/main/webapp/app/overview/course-competencies/course-competencies.component.html +++ b/src/main/webapp/app/overview/course-competencies/course-competencies.component.html @@ -14,12 +14,12 @@
- {{ 'artemisApp.competency.prerequisite.title' | artemisTranslate }}: {{ countPrerequisites }} + {{ 'artemisApp.prerequisite.title' | artemisTranslate }}: {{ countPrerequisites }}
@if (!isCollapsed) {
@for (prerequisite of prerequisites; track identify(i, prerequisite); let i = $index) { - + }
} diff --git a/src/main/webapp/app/overview/course-registration/course-registration-prerequisites-modal/course-prerequisites-modal.component.html b/src/main/webapp/app/overview/course-registration/course-registration-prerequisites-modal/course-prerequisites-modal.component.html index ed3bc3579d0b..0c93b90f1093 100644 --- a/src/main/webapp/app/overview/course-registration/course-registration-prerequisites-modal/course-prerequisites-modal.component.html +++ b/src/main/webapp/app/overview/course-registration/course-registration-prerequisites-modal/course-prerequisites-modal.component.html @@ -1,5 +1,5 @@ } @else {
- @for (prerequisite of prerequisites; track identify(i, prerequisite); let i = $index) { + @for (prerequisite of prerequisites; track prerequisite.id; let i = $index) { }
diff --git a/src/main/webapp/app/overview/course-registration/course-registration-prerequisites-modal/course-prerequisites-modal.component.ts b/src/main/webapp/app/overview/course-registration/course-registration-prerequisites-modal/course-prerequisites-modal.component.ts index bcf6f337a4c2..db8bb138101d 100644 --- a/src/main/webapp/app/overview/course-registration/course-registration-prerequisites-modal/course-prerequisites-modal.component.ts +++ b/src/main/webapp/app/overview/course-registration/course-registration-prerequisites-modal/course-prerequisites-modal.component.ts @@ -1,9 +1,9 @@ import { Component, Input, OnInit } from '@angular/core'; import { AlertService } from 'app/core/util/alert.service'; import { finalize } from 'rxjs/operators'; -import { Competency } from 'app/entities/competency.model'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { PrerequisiteService } from 'app/course/competencies/prerequisite.service'; +import { Prerequisite } from 'app/entities/prerequisite.model'; @Component({ selector: 'jhi-course-prerequisites-modal', @@ -15,7 +15,7 @@ export class CoursePrerequisitesModalComponent implements OnInit { courseId: number; isLoading = false; - prerequisites: Competency[] = []; + prerequisites: Prerequisite[] = []; constructor( private alertService: AlertService, @@ -52,15 +52,6 @@ export class CoursePrerequisitesModalComponent implements OnInit { }); } - /** - * Calculates a unique identity for each competency card shown in the component - * @param index The index in the list - * @param competency The competency of the current iteration - */ - identify(index: number, competency: Competency) { - return `${index}-${competency.id}`; - } - /** * Dismisses the currently active modal */ diff --git a/src/test/javascript/spec/component/course-registration/course-prerequisites-modal/course-prerequisites-modal.component.spec.ts b/src/test/javascript/spec/component/course-registration/course-prerequisites-modal/course-prerequisites-modal.component.spec.ts index ac6e45c5f384..4512a17acbc5 100644 --- a/src/test/javascript/spec/component/course-registration/course-prerequisites-modal/course-prerequisites-modal.component.spec.ts +++ b/src/test/javascript/spec/component/course-registration/course-prerequisites-modal/course-prerequisites-modal.component.spec.ts @@ -1,7 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { MockPipe, MockProvider } from 'ng-mocks'; -import { CompetencyService } from 'app/course/competencies/competency.service'; import { of } from 'rxjs'; import { By } from '@angular/platform-browser'; import { CoursePrerequisitesModalComponent } from 'app/overview/course-registration/course-registration-prerequisites-modal/course-prerequisites-modal.component'; @@ -26,7 +25,7 @@ describe('CoursePrerequisitesModal', () => { declarations: [CoursePrerequisitesModalComponent, CompetencyCardStubComponent, MockPipe(ArtemisTranslatePipe)], providers: [ MockProvider(AlertService), - MockProvider(CompetencyService), + MockProvider(PrerequisiteService), { provide: NgbActiveModal, useValue: activeModalStub, From d1cf05f0e59c442521423a78de979ede6a99dd74 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Fri, 31 May 2024 18:39:47 +0200 Subject: [PATCH 31/78] Add tests for endpoints --- .../lecture/PrerequisiteIntegrationTest.java | 47 +++++++++++++++++++ .../competencies/prerequisite.service.spec.ts | 26 ++++++++++ 2 files changed, 73 insertions(+) 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 58eb85f64640..b875f1a2a15f 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 @@ -16,9 +16,12 @@ import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.DomainObject; +import de.tum.in.www1.artemis.domain.competency.CompetencyTaxonomy; +import de.tum.in.www1.artemis.domain.competency.Prerequisite; import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.PrerequisiteRepository; import de.tum.in.www1.artemis.user.UserUtilService; +import de.tum.in.www1.artemis.web.rest.dto.competency.PrerequisiteRequestDTO; import de.tum.in.www1.artemis.web.rest.dto.competency.PrerequisiteResponseDTO; class PrerequisiteIntegrationTest extends AbstractSpringIntegrationIndependentTest { @@ -102,6 +105,50 @@ void shouldNotReturnPrerequisitesForStudentNotInCourse() throws Exception { } } + @Nested + class CreatePrerequisite { + + private static String url(long courseId) { + return PrerequisiteIntegrationTest.baseUrl(courseId); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") + void shouldCreatePrerequisite() throws Exception { + var prerequisite = new Prerequisite("title", "description", null, 66, CompetencyTaxonomy.ANALYZE, false); + var requestBody = prerequisiteUtilService.prerequisiteToRequestDTO(prerequisite); + + var actualPrerequisite = request.postWithResponseBody(url(course.getId()), requestBody, PrerequisiteResponseDTO.class, HttpStatus.CREATED); + + var expectedPrerequisite = new PrerequisiteResponseDTO(actualPrerequisite.id(), prerequisite.getTitle(), prerequisite.getDescription(), prerequisite.getTaxonomy(), + prerequisite.getSoftDueDate(), prerequisite.getMasteryThreshold(), prerequisite.isOptional(), null); + assertThat(actualPrerequisite).usingRecursiveComparison().ignoringFields("id").isEqualTo(expectedPrerequisite); + } + + } + + @Nested + class UpdatePrerequisite { + + private static String url(long courseId, long prerequisiteId) { + return PrerequisiteIntegrationTest.baseUrl(courseId) + "/" + prerequisiteId; + } + + @Test + @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") + void shouldUpdatePrerequisite() throws Exception { + var existingPrerequisite = prerequisiteUtilService.createPrerequisite(course); + var updateDTO = new PrerequisiteRequestDTO("new title", "new description", CompetencyTaxonomy.ANALYZE, null, 1, true); + + var updatedPrerequisite = request.putWithResponseBody(url(course.getId(), existingPrerequisite.getId()), updateDTO, PrerequisiteResponseDTO.class, HttpStatus.OK); + + assertThat(updatedPrerequisite).usingRecursiveComparison().comparingOnlyFields("title", "description", "taxonomy", "softDueDate", "masteryThreshold", "optional") + .isEqualTo(updateDTO); + assertThat(updatedPrerequisite.id()).isEqualTo(existingPrerequisite.getId()); + } + + } + @Nested class DeletePrerequisite { diff --git a/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts b/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts index 49cab6e80f95..ae828e32941f 100644 --- a/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts +++ b/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts @@ -62,4 +62,30 @@ describe('PrerequisiteService', () => { expect(result).toBeTrue(); })); + + it('should create a prerequisite', fakeAsync(() => { + let actualPrerequisite: Prerequisite | undefined; + const expectedPrerequisite: Prerequisite = { id: 1, title: 'newTitle', description: 'newDescription' }; + const returnedFromService: Prerequisite = { ...expectedPrerequisite }; + + prerequisiteService.createPrerequisite({ title: 'newTitle', description: 'newDescription' }, 1, 1).subscribe((resp) => (actualPrerequisite = resp)); + const req = httpTestingController.expectOne({ method: 'POST' }); + req.flush(returnedFromService); + tick(); + + expect(actualPrerequisite).toEqual(expectedPrerequisite); + })); + + it('should update a prerequisite', fakeAsync(() => { + let actualPrerequisite: Prerequisite | undefined; + const expectedPrerequisite: Prerequisite = { id: 1, title: 'newTitle', description: 'newDescription' }; + const returnedFromService: Prerequisite = { ...expectedPrerequisite }; + + prerequisiteService.createPrerequisite({ title: 'newTitle', description: 'newDescription' }, 1, 1).subscribe((resp) => (actualPrerequisite = resp)); + const req = httpTestingController.expectOne({ method: 'POST' }); + req.flush(returnedFromService); + tick(); + + expect(actualPrerequisite).toEqual(expectedPrerequisite); + })); }); From d42aadbb737ec89e59ad86d001d3e26434660792 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Fri, 31 May 2024 18:50:17 +0200 Subject: [PATCH 32/78] Remove unused code --- .../competency/PrerequisiteRequestDTO.java | 20 ---------- .../competencies/prerequisite.service.ts | 37 ++----------------- .../webapp/app/entities/prerequisite.model.ts | 11 +----- 3 files changed, 4 insertions(+), 64 deletions(-) delete mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteRequestDTO.java diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteRequestDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteRequestDTO.java deleted file mode 100644 index 555bec9c6fe2..000000000000 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteRequestDTO.java +++ /dev/null @@ -1,20 +0,0 @@ -package de.tum.in.www1.artemis.web.rest.dto.competency; - -import java.time.ZonedDateTime; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Size; - -import com.fasterxml.jackson.annotation.JsonInclude; - -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; - -/** - * DTO used to send create/update requests regarding {@link Prerequisite} objects. - */ -@JsonInclude(JsonInclude.Include.NON_EMPTY) -public record PrerequisiteRequestDTO(@NotBlank @Size(min = 1, max = CourseCompetency.MAX_TITLE_LENGTH) String title, String description, CompetencyTaxonomy taxonomy, - ZonedDateTime softDueDate, int masteryThreshold, boolean optional) { -} diff --git a/src/main/webapp/app/course/competencies/prerequisite.service.ts b/src/main/webapp/app/course/competencies/prerequisite.service.ts index 81c222458001..84ebc6ea0170 100644 --- a/src/main/webapp/app/course/competencies/prerequisite.service.ts +++ b/src/main/webapp/app/course/competencies/prerequisite.service.ts @@ -1,9 +1,9 @@ -import { Prerequisite, PrerequisiteRequestDTO, PrerequisiteResponseDTO } from 'app/entities/prerequisite.model'; +import { Prerequisite, PrerequisiteResponseDTO } from 'app/entities/prerequisite.model'; import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, map } from 'rxjs'; -import { CourseCompetency, DEFAULT_MASTERY_THRESHOLD } from 'app/entities/competency.model'; -import { convertDateFromClient, convertDateFromServer } from 'app/utils/date.utils'; +import { CourseCompetency } from 'app/entities/competency.model'; +import { convertDateFromServer } from 'app/utils/date.utils'; @Injectable({ providedIn: 'root', @@ -25,20 +25,6 @@ export class PrerequisiteService { //TODO: send title to entityTitleService when we allow prerequisite detail view. } - createPrerequisite(prerequisite: Prerequisite, prerequisiteId: number, courseId: number): Observable { - const prerequisiteDTO = this.convertToRequestDTO(prerequisite); - return this.httpClient - .post(`${this.resourceURL}/courses/${courseId}/competencies/prerequisites/${prerequisiteId}`, prerequisiteDTO, { observe: 'response' }) - .pipe(map((resp) => (resp.body ? this.convertResponseDTOToPrerequisite(resp.body) : undefined))); - } - - updatePrerequisite(prerequisite: Prerequisite, prerequisiteId: number, courseId: number): Observable { - const prerequisiteDTO = this.convertToRequestDTO(prerequisite); - return this.httpClient - .post(`${this.resourceURL}/courses/${courseId}/competencies/prerequisites/${prerequisiteId}`, prerequisiteDTO, { observe: 'response' }) - .pipe(map((resp) => (resp.body ? this.convertResponseDTOToPrerequisite(resp.body) : undefined))); - } - deletePrerequisite(prerequisiteId: number, courseId: number) { return this.httpClient.delete(`${this.resourceURL}/courses/${courseId}/competencies/prerequisites/${prerequisiteId}`, { observe: 'response' }); } @@ -56,23 +42,6 @@ export class PrerequisiteService { ); } - /** - * Converts a Prerequisite to a PrerequisiteRequestDTO and converts the date to a string - * @param prerequisite the prerequisite to convert - * @return the PrerequisiteRequestDTO - */ - convertToRequestDTO(prerequisite: Prerequisite) { - const dto: PrerequisiteRequestDTO = { - title: prerequisite.title, - description: prerequisite.description, - taxonomy: prerequisite.taxonomy, - masteryThreshold: prerequisite.masteryThreshold ?? DEFAULT_MASTERY_THRESHOLD, - optional: prerequisite.optional, - softDueDate: convertDateFromClient(prerequisite.softDueDate), - }; - return dto; - } - /** * Converts a PrerequisiteResponseDTO to a Prerequisite * It converts the softDueDate to dayjs and the linkedCourseCompetencyDTO to a CourseCompetency diff --git a/src/main/webapp/app/entities/prerequisite.model.ts b/src/main/webapp/app/entities/prerequisite.model.ts index aa47bc7c31bd..bc33c00ac7c4 100644 --- a/src/main/webapp/app/entities/prerequisite.model.ts +++ b/src/main/webapp/app/entities/prerequisite.model.ts @@ -1,4 +1,4 @@ -import { CompetencyTaxonomy, CourseCompetency } from 'app/entities/competency.model'; +import { CourseCompetency } from 'app/entities/competency.model'; export interface Prerequisite extends CourseCompetency {} @@ -12,12 +12,3 @@ export interface LinkedCourseCompetencyDTO { courseTitle: string; semester: string; } - -export interface PrerequisiteRequestDTO { - title?: string; - description?: string; - taxonomy?: CompetencyTaxonomy; - softDueDate?: string; - masteryThreshold?: number; - optional?: boolean; -} From a1a27ce057a46d68cbac8d2f9322d693847bbaa5 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Fri, 31 May 2024 18:53:41 +0200 Subject: [PATCH 33/78] Remove more unused code --- .../artemis/competency/PrerequisiteUtilService.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/test/java/de/tum/in/www1/artemis/competency/PrerequisiteUtilService.java b/src/test/java/de/tum/in/www1/artemis/competency/PrerequisiteUtilService.java index 7f511576f8b5..c7c4579e68d2 100644 --- a/src/test/java/de/tum/in/www1/artemis/competency/PrerequisiteUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/competency/PrerequisiteUtilService.java @@ -9,7 +9,6 @@ import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.competency.Prerequisite; import de.tum.in.www1.artemis.repository.PrerequisiteRepository; -import de.tum.in.www1.artemis.web.rest.dto.competency.PrerequisiteRequestDTO; @Service public class PrerequisiteUtilService { @@ -60,15 +59,4 @@ public List createPrerequisites(Course course, int numberOfPrerequ } return prerequisites; } - - /** - * Creates a PrerequisiteRequestDTO from a prerequisite - * - * @param prerequisite the prerequisite to conver - * @return the created PrerequisiteRequestDTO - */ - public PrerequisiteRequestDTO prerequisiteToRequestDTO(Prerequisite prerequisite) { - return new PrerequisiteRequestDTO(prerequisite.getTitle(), prerequisite.getDescription(), prerequisite.getTaxonomy(), prerequisite.getSoftDueDate(), - prerequisite.getMasteryThreshold(), prerequisite.isOptional()); - } } From 97a8bed653d25c1315dd3b2dc108cd3bd2a941e6 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Fri, 31 May 2024 19:04:43 +0200 Subject: [PATCH 34/78] Add server tests --- .../lecture/PrerequisiteIntegrationTest.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) 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 58eb85f64640..3c8748e0e479 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 @@ -157,6 +157,32 @@ void shouldImportPrerequisites() throws Exception { assertThat(actualPrerequisites).hasSameSizeAs(expectedPrerequisites); } + @Test + @WithMockUser(username = "admin", roles = "ADMIN") + void shouldImportPrerequisitesAsAdmin() throws Exception { + var prerequisites = prerequisiteUtilService.createPrerequisites(course, 5); + prerequisites.addAll(prerequisiteUtilService.createPrerequisites(course, 5)); + var prerequisiteIds = prerequisites.stream().map(DomainObject::getId).toList(); + + var expectedPrerequisites = prerequisites.stream().map(prerequisite -> { + prerequisite.setLinkedCourseCompetency(prerequisite); + return PrerequisiteResponseDTO.of(prerequisite); + }).toList(); + + var actualPrerequisites = request.postListWithResponseBody(url(course2.getId()), prerequisiteIds, PrerequisiteResponseDTO.class, HttpStatus.CREATED); + + assertThat(actualPrerequisites).usingRecursiveFieldByFieldElementComparatorIgnoringFields("id").containsAll(expectedPrerequisites); + assertThat(actualPrerequisites).hasSameSizeAs(expectedPrerequisites); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") + void shouldReturnNotFoundIfCompetencyDoesNotExist() throws Exception { + var prerequisite = prerequisiteUtilService.createPrerequisite(course); + // one of the competencies does not exist, this should lead to a not found error + request.post(url(course2.getId()), List.of(-1000, prerequisite.getId()), HttpStatus.NOT_FOUND); + } + @Test @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") void shouldReturnBadRequestWhenImportingFromSameCourse() throws Exception { From 109cafb1235c89c613890bc40406066206f9261c Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Fri, 31 May 2024 19:05:40 +0200 Subject: [PATCH 35/78] Revert "Remove more unused code" This reverts commit a1a27ce057a46d68cbac8d2f9322d693847bbaa5. --- .../artemis/competency/PrerequisiteUtilService.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test/java/de/tum/in/www1/artemis/competency/PrerequisiteUtilService.java b/src/test/java/de/tum/in/www1/artemis/competency/PrerequisiteUtilService.java index c7c4579e68d2..7f511576f8b5 100644 --- a/src/test/java/de/tum/in/www1/artemis/competency/PrerequisiteUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/competency/PrerequisiteUtilService.java @@ -9,6 +9,7 @@ import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.competency.Prerequisite; import de.tum.in.www1.artemis.repository.PrerequisiteRepository; +import de.tum.in.www1.artemis.web.rest.dto.competency.PrerequisiteRequestDTO; @Service public class PrerequisiteUtilService { @@ -59,4 +60,15 @@ public List createPrerequisites(Course course, int numberOfPrerequ } return prerequisites; } + + /** + * Creates a PrerequisiteRequestDTO from a prerequisite + * + * @param prerequisite the prerequisite to conver + * @return the created PrerequisiteRequestDTO + */ + public PrerequisiteRequestDTO prerequisiteToRequestDTO(Prerequisite prerequisite) { + return new PrerequisiteRequestDTO(prerequisite.getTitle(), prerequisite.getDescription(), prerequisite.getTaxonomy(), prerequisite.getSoftDueDate(), + prerequisite.getMasteryThreshold(), prerequisite.isOptional()); + } } From b253e261288e1488adaee7ec446b1d2bffc86ffc Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Fri, 31 May 2024 19:05:41 +0200 Subject: [PATCH 36/78] Revert "Remove unused code" This reverts commit d42aadbb737ec89e59ad86d001d3e26434660792. --- .../competency/PrerequisiteRequestDTO.java | 20 ++++++++++ .../competencies/prerequisite.service.ts | 37 +++++++++++++++++-- .../webapp/app/entities/prerequisite.model.ts | 11 +++++- 3 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteRequestDTO.java diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteRequestDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteRequestDTO.java new file mode 100644 index 000000000000..555bec9c6fe2 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteRequestDTO.java @@ -0,0 +1,20 @@ +package de.tum.in.www1.artemis.web.rest.dto.competency; + +import java.time.ZonedDateTime; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +import com.fasterxml.jackson.annotation.JsonInclude; + +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; + +/** + * DTO used to send create/update requests regarding {@link Prerequisite} objects. + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record PrerequisiteRequestDTO(@NotBlank @Size(min = 1, max = CourseCompetency.MAX_TITLE_LENGTH) String title, String description, CompetencyTaxonomy taxonomy, + ZonedDateTime softDueDate, int masteryThreshold, boolean optional) { +} diff --git a/src/main/webapp/app/course/competencies/prerequisite.service.ts b/src/main/webapp/app/course/competencies/prerequisite.service.ts index 84ebc6ea0170..81c222458001 100644 --- a/src/main/webapp/app/course/competencies/prerequisite.service.ts +++ b/src/main/webapp/app/course/competencies/prerequisite.service.ts @@ -1,9 +1,9 @@ -import { Prerequisite, PrerequisiteResponseDTO } from 'app/entities/prerequisite.model'; +import { Prerequisite, PrerequisiteRequestDTO, PrerequisiteResponseDTO } from 'app/entities/prerequisite.model'; import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, map } from 'rxjs'; -import { CourseCompetency } from 'app/entities/competency.model'; -import { convertDateFromServer } from 'app/utils/date.utils'; +import { CourseCompetency, DEFAULT_MASTERY_THRESHOLD } from 'app/entities/competency.model'; +import { convertDateFromClient, convertDateFromServer } from 'app/utils/date.utils'; @Injectable({ providedIn: 'root', @@ -25,6 +25,20 @@ export class PrerequisiteService { //TODO: send title to entityTitleService when we allow prerequisite detail view. } + createPrerequisite(prerequisite: Prerequisite, prerequisiteId: number, courseId: number): Observable { + const prerequisiteDTO = this.convertToRequestDTO(prerequisite); + return this.httpClient + .post(`${this.resourceURL}/courses/${courseId}/competencies/prerequisites/${prerequisiteId}`, prerequisiteDTO, { observe: 'response' }) + .pipe(map((resp) => (resp.body ? this.convertResponseDTOToPrerequisite(resp.body) : undefined))); + } + + updatePrerequisite(prerequisite: Prerequisite, prerequisiteId: number, courseId: number): Observable { + const prerequisiteDTO = this.convertToRequestDTO(prerequisite); + return this.httpClient + .post(`${this.resourceURL}/courses/${courseId}/competencies/prerequisites/${prerequisiteId}`, prerequisiteDTO, { observe: 'response' }) + .pipe(map((resp) => (resp.body ? this.convertResponseDTOToPrerequisite(resp.body) : undefined))); + } + deletePrerequisite(prerequisiteId: number, courseId: number) { return this.httpClient.delete(`${this.resourceURL}/courses/${courseId}/competencies/prerequisites/${prerequisiteId}`, { observe: 'response' }); } @@ -42,6 +56,23 @@ export class PrerequisiteService { ); } + /** + * Converts a Prerequisite to a PrerequisiteRequestDTO and converts the date to a string + * @param prerequisite the prerequisite to convert + * @return the PrerequisiteRequestDTO + */ + convertToRequestDTO(prerequisite: Prerequisite) { + const dto: PrerequisiteRequestDTO = { + title: prerequisite.title, + description: prerequisite.description, + taxonomy: prerequisite.taxonomy, + masteryThreshold: prerequisite.masteryThreshold ?? DEFAULT_MASTERY_THRESHOLD, + optional: prerequisite.optional, + softDueDate: convertDateFromClient(prerequisite.softDueDate), + }; + return dto; + } + /** * Converts a PrerequisiteResponseDTO to a Prerequisite * It converts the softDueDate to dayjs and the linkedCourseCompetencyDTO to a CourseCompetency diff --git a/src/main/webapp/app/entities/prerequisite.model.ts b/src/main/webapp/app/entities/prerequisite.model.ts index bc33c00ac7c4..aa47bc7c31bd 100644 --- a/src/main/webapp/app/entities/prerequisite.model.ts +++ b/src/main/webapp/app/entities/prerequisite.model.ts @@ -1,4 +1,4 @@ -import { CourseCompetency } from 'app/entities/competency.model'; +import { CompetencyTaxonomy, CourseCompetency } from 'app/entities/competency.model'; export interface Prerequisite extends CourseCompetency {} @@ -12,3 +12,12 @@ export interface LinkedCourseCompetencyDTO { courseTitle: string; semester: string; } + +export interface PrerequisiteRequestDTO { + title?: string; + description?: string; + taxonomy?: CompetencyTaxonomy; + softDueDate?: string; + masteryThreshold?: number; + optional?: boolean; +} From ea334780255f39b69cb039ec627b349a2666d011 Mon Sep 17 00:00:00 2001 From: Raphael Stief <118574504+rstief@users.noreply.github.com> Date: Sun, 2 Jun 2024 12:34:40 +0200 Subject: [PATCH 37/78] Update src/main/java/de/tum/in/www1/artemis/web/rest/competency/PrerequisiteResource.java --- .../artemis/web/rest/competency/PrerequisiteResource.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 0204821fb6a4..e69c3acfb1b4 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 @@ -55,10 +55,11 @@ public PrerequisiteResource(PrerequisiteService prerequisiteService, Prerequisit } /** - * GET /courses/:courseId/competencies/prerequisites + * GET /courses/:courseId/competencies/prerequisites : Gets all prerequisite competencies for a course. + * This endpoint allows all students to view prerequisites of a course if self-enrollment is activated (and thus only uses @EnforceAtLeastStudent) * - * @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 + * @param courseId the id of the course for which the prerequisites should be fetched for + * @return the ResponseEntity with status 200 (OK) and with body the found prerequisites */ @GetMapping("courses/{courseId}/competencies/prerequisites") @EnforceAtLeastStudent From cac153ac39a4fb5d944eaf06558c9692547004c2 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Sun, 2 Jun 2024 12:54:02 +0200 Subject: [PATCH 38/78] Add JsonInclude Annotation --- .../artemis/web/rest/dto/competency/PrerequisiteResponseDTO.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteResponseDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteResponseDTO.java index e504fa22cb19..b8e801cb903b 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteResponseDTO.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteResponseDTO.java @@ -32,6 +32,7 @@ public static PrerequisiteResponseDTO of(Prerequisite prerequisite) { prerequisite.getMasteryThreshold(), prerequisite.isOptional(), linkedCourseCompetencyDTO); } + @JsonInclude(JsonInclude.Include.NON_EMPTY) private record LinkedCourseCompetencyDTO(long id, long courseId, String courseTitle, String semester) { } From f799758882a2ff85a2c62696188775c48b9b535d Mon Sep 17 00:00:00 2001 From: Raphael Stief <118574504+rstief@users.noreply.github.com> Date: Sun, 2 Jun 2024 15:04:09 +0200 Subject: [PATCH 39/78] Fix search empty search table translation --- .../import-course-competencies.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/course/competencies/import-competencies/import-course-competencies.component.html b/src/main/webapp/app/course/competencies/import-competencies/import-course-competencies.component.html index c8bca019d304..e0e3c59cb215 100644 --- a/src/main/webapp/app/course/competencies/import-competencies/import-course-competencies.component.html +++ b/src/main/webapp/app/course/competencies/import-competencies/import-course-competencies.component.html @@ -9,7 +9,7 @@

< } @else { - + }

@if (selectedCourseCompetencies.resultsOnPage?.length) { From 8d632dadfcf8028ab148585529bdd5a7e00ed400 Mon Sep 17 00:00:00 2001 From: Raphael Stief <118574504+rstief@users.noreply.github.com> Date: Sun, 2 Jun 2024 18:28:43 +0200 Subject: [PATCH 40/78] Apply suggestions from code review (german translations) Co-authored-by: Jan-Thurner <107639007+Jan-Thurner@users.noreply.github.com> --- src/main/webapp/i18n/de/competency.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/i18n/de/competency.json b/src/main/webapp/i18n/de/competency.json index 3440e657916f..8ddf3723caac 100644 --- a/src/main/webapp/i18n/de/competency.json +++ b/src/main/webapp/i18n/de/competency.json @@ -193,8 +193,8 @@ "title": "Voraussetzungen", "empty": "Es existieren keine Voraussetzungen für diesen Kurs", "manage": { - "importButton": "Import prerequisites", - "importFromCoursesButton": "Import from other courses", + "importButton": "Voraussetzungen importieren", + "importFromCoursesButton": "Aus anderen Kursen importieren", "deleted": "Voraussetzung gelöscht", "delete": { "question": "Willst du wirklich die Voraussetzung {{ title }} löschen? Du kannst diese Aktion nicht rückgängig machen!", From 19e438f135c0c3e7a5ce67a58147350354a9e000 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Mon, 3 Jun 2024 13:04:28 +0200 Subject: [PATCH 41/78] Revert rename competency -> course_competency, reduce size of discriminator --- .../artemis/domain/competency/Competency.java | 2 +- .../domain/competency/CourseCompetency.java | 2 +- .../artemis/domain/competency/Prerequisite.java | 2 +- .../CompetencyRelationRepository.java | 2 +- .../changelog/20240523180900_changelog.xml | 17 ++++++++--------- 5 files changed, 12 insertions(+), 13 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 2d65db08564b..64095720b88e 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 @@ -22,7 +22,7 @@ import de.tum.in.www1.artemis.domain.lecture.LectureUnit; @Entity -@DiscriminatorValue("COMPETENCY") +@DiscriminatorValue("C") public class Competency extends CourseCompetency { // TODO: move properties (linkedStandardizedCompetency, exercises, lectureUnits, userProgress, learningPaths) to CourseCompetency when refactoring 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 e1bb8ded15cb..f4c08381a007 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 @@ -25,7 +25,7 @@ * It is extended by {@link Competency} and {@link Prerequisite} */ @Entity -@Table(name = "course_competency") +@Table(name = "competency") @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "discriminator", discriminatorType = DiscriminatorType.STRING) @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) 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 33964e425c00..498cfd68c1f1 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 @@ -6,7 +6,7 @@ import jakarta.persistence.Entity; @Entity -@DiscriminatorValue("PREREQUISITE") +@DiscriminatorValue("P") public class Prerequisite extends CourseCompetency { public Prerequisite(String title, String description, ZonedDateTime softDueDate, Integer masteryThreshold, CompetencyTaxonomy taxonomy, boolean optional) { diff --git a/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRelationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRelationRepository.java index 892751724b63..794e7ef2520f 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRelationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRelationRepository.java @@ -82,7 +82,7 @@ long countRelationsOfTypeBetweenCompetencyGroups(@Param("competencyTailIds") Set @Query(value = """ WITH RECURSIVE transitive_closure(id) AS ( - (SELECT competency.id FROM course_competency competency WHERE competency.id = :competencyId) + (SELECT competency.id FROM competency WHERE competency.id = :competencyId) UNION ( SELECT CASE diff --git a/src/main/resources/config/liquibase/changelog/20240523180900_changelog.xml b/src/main/resources/config/liquibase/changelog/20240523180900_changelog.xml index c36902baa2e5..c961c50d75aa 100644 --- a/src/main/resources/config/liquibase/changelog/20240523180900_changelog.xml +++ b/src/main/resources/config/liquibase/changelog/20240523180900_changelog.xml @@ -4,15 +4,15 @@ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> - + - + - UPDATE competency SET discriminator = 'COMPETENCY' - + UPDATE competency SET discriminator = 'C' + UPDATE competency SET mastery_threshold = 100 WHERE mastery_threshold IS NULL @@ -20,10 +20,10 @@ @@ -38,7 +38,7 @@ BEGIN max_id := (SELECT (MAX(id)) as id FROM competency); INSERT INTO competency (id, description, title, course_id, taxonomy, mastery_threshold, soft_due_date, optional, linked_standardized_competency_id, discriminator, linked_course_competency_id) - SELECT (max_id + row_number() over ()) as id, description, title, competency_course.course_id AS course_id, taxonomy, mastery_threshold, soft_due_date, optional, linked_standardized_competency_id, 'PREREQUISITE', competency.id as linked_competency_id + SELECT (max_id + row_number() over ()) as id, description, title, competency_course.course_id AS course_id, taxonomy, mastery_threshold, soft_due_date, optional, linked_standardized_competency_id, 'P', competency.id as linked_competency_id FROM competency RIGHT JOIN competency_course ON competency.id = competency_course.competency_id; END $$; @@ -52,13 +52,12 @@ SELECT @max_id := MAX(id) FROM competency; INSERT INTO competency (id, description, title, course_id, taxonomy, mastery_threshold, soft_due_date, optional, linked_standardized_competency_id, discriminator, linked_course_competency_id) - SELECT (@max_id := @max_id + 1) as id, description, title, competency_course.course_id as course_id, taxonomy, mastery_threshold, soft_due_date, optional, linked_standardized_competency_id, 'PREREQUISITE', competency.id as linked_competency_id + SELECT (@max_id := @max_id + 1) as id, description, title, competency_course.course_id as course_id, taxonomy, mastery_threshold, soft_due_date, optional, linked_standardized_competency_id, 'P', competency.id as linked_competency_id FROM competency RIGHT JOIN competency_course on competency.id = competency_course.competency_id; - From a397eafda4e2a2615c5ae8ce014a29794d0ff05f Mon Sep 17 00:00:00 2001 From: Raphael Stief <118574504+rstief@users.noreply.github.com> Date: Mon, 3 Jun 2024 20:36:27 +0200 Subject: [PATCH 42/78] Apply suggestions from code review --- .../create-competency/create-competency.component.ts | 1 - src/main/webapp/app/course/competencies/prerequisite.service.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) 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 14b2e3e83cd9..c250e296fbd5 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 @@ -32,7 +32,6 @@ export class CreateCompetencyComponent implements OnInit { ) {} ngOnInit(): void { - this.competencyToCreate = {}; this.isLoading = true; this.activatedRoute .parent!.parent!.paramMap.pipe( diff --git a/src/main/webapp/app/course/competencies/prerequisite.service.ts b/src/main/webapp/app/course/competencies/prerequisite.service.ts index 84ebc6ea0170..04128b385977 100644 --- a/src/main/webapp/app/course/competencies/prerequisite.service.ts +++ b/src/main/webapp/app/course/competencies/prerequisite.service.ts @@ -48,7 +48,7 @@ export class PrerequisiteService { * @param prerequisiteDTO PrerequisiteResponseDTO * @return the Prerequisite */ - convertResponseDTOToPrerequisite(prerequisiteDTO: PrerequisiteResponseDTO): Prerequisite { + private static convertResponseDTOToPrerequisite(prerequisiteDTO: PrerequisiteResponseDTO): Prerequisite { let linkedCourseCompetency: CourseCompetency | undefined = undefined; const softDueDate = convertDateFromServer(prerequisiteDTO.softDueDate); From e75000669c1a8d36419d903cc973f4a9ccc36125 Mon Sep 17 00:00:00 2001 From: Raphael Stief <118574504+rstief@users.noreply.github.com> Date: Mon, 3 Jun 2024 21:18:27 +0200 Subject: [PATCH 43/78] Update src/main/webapp/app/course/competencies/prerequisite.service.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/main/webapp/app/course/competencies/prerequisite.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/course/competencies/prerequisite.service.ts b/src/main/webapp/app/course/competencies/prerequisite.service.ts index 04128b385977..6edb5bd231f1 100644 --- a/src/main/webapp/app/course/competencies/prerequisite.service.ts +++ b/src/main/webapp/app/course/competencies/prerequisite.service.ts @@ -37,7 +37,7 @@ export class PrerequisiteService { if (!resp.body) { return []; } - return resp.body.map((prerequisiteDTO) => this.convertResponseDTOToPrerequisite(prerequisiteDTO)); + return resp.body.map((prerequisiteDTO) => PrerequisiteService.convertResponseDTOToPrerequisite(prerequisiteDTO)); }), ); } From 02f3bde1857b5ab563e30fe8ded7b15a7e256a4f Mon Sep 17 00:00:00 2001 From: Raphael Stief <118574504+rstief@users.noreply.github.com> Date: Mon, 3 Jun 2024 21:18:45 +0200 Subject: [PATCH 44/78] Update src/main/webapp/app/course/competencies/prerequisite.service.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/main/webapp/app/course/competencies/prerequisite.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/course/competencies/prerequisite.service.ts b/src/main/webapp/app/course/competencies/prerequisite.service.ts index 6edb5bd231f1..581a94960397 100644 --- a/src/main/webapp/app/course/competencies/prerequisite.service.ts +++ b/src/main/webapp/app/course/competencies/prerequisite.service.ts @@ -19,7 +19,7 @@ export class PrerequisiteService { if (!resp.body) { return []; } - return resp.body.map((prerequisiteDTO) => this.convertResponseDTOToPrerequisite(prerequisiteDTO)); + return resp.body.map((prerequisiteDTO) => PrerequisiteService.convertResponseDTOToPrerequisite(prerequisiteDTO)); }), ); //TODO: send title to entityTitleService when we allow prerequisite detail view. From c9fcb1ba1ffb6ced80b5f0b1f6c87b95982e443d Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Tue, 4 Jun 2024 17:01:50 +0200 Subject: [PATCH 45/78] Fix server test --- .../competency/StandardizedCompetencyIntegrationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/de/tum/in/www1/artemis/competency/StandardizedCompetencyIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/competency/StandardizedCompetencyIntegrationTest.java index a617ddd52441..f9b23df42e16 100644 --- a/src/test/java/de/tum/in/www1/artemis/competency/StandardizedCompetencyIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/competency/StandardizedCompetencyIntegrationTest.java @@ -206,7 +206,7 @@ void shouldDeleteCompetency() throws Exception { @WithMockUser(username = "admin", roles = "ADMIN") void shouldDeleteCompetencyWithLinkedCompetency() throws Exception { long deletedId = standardizedCompetency.getId(); - Competency competency = new Competency("title", "description", null, null, null, false); + Competency competency = new Competency("title", "description", null, 100, null, false); competency.setLinkedStandardizedCompetency(standardizedCompetency); competency = competencyRepository.save(competency); From f743a71d5be3af483f48c2450e7972f09285b6b3 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Tue, 4 Jun 2024 17:41:17 +0200 Subject: [PATCH 46/78] Disallow import of competencies that have already been imported as prerequisites --- .../import-course-competencies.component.ts | 29 +++++++++++-------- .../import-prerequisites.component.ts | 5 +--- ...port-course-competencies.component.spec.ts | 21 +++++++++++--- 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/main/webapp/app/course/competencies/import-competencies/import-course-competencies.component.ts b/src/main/webapp/app/course/competencies/import-competencies/import-course-competencies.component.ts index 534f0deaa0cc..c88ded806dfd 100644 --- a/src/main/webapp/app/course/competencies/import-competencies/import-course-competencies.component.ts +++ b/src/main/webapp/app/course/competencies/import-competencies/import-course-competencies.component.ts @@ -11,6 +11,8 @@ import { ButtonType } from 'app/shared/components/button.component'; import { ActivatedRoute, Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { HttpErrorResponse } from '@angular/common/http'; +import { PrerequisiteService } from 'app/course/competencies/prerequisite.service'; +import { forkJoin } from 'rxjs'; /** * An abstract component used to import course competencies. Its concrete implementations are @@ -73,23 +75,26 @@ export abstract class ImportCourseCompetenciesComponent implements OnInit, Compo protected readonly activatedRoute: ActivatedRoute = inject(ActivatedRoute); protected readonly router: Router = inject(Router); protected readonly competencyService: CompetencyService = inject(CompetencyService); + protected readonly prerequisiteService: PrerequisiteService = inject(PrerequisiteService); protected readonly alertService: AlertService = inject(AlertService); private readonly translateService: TranslateService = inject(TranslateService); private readonly sortingService: SortService = inject(SortService); ngOnInit(): void { - this.activatedRoute.params.subscribe((params) => { - this.courseId = Number(params['courseId']); - this.performSearch(); - //load competencies of this course to disable their import buttons - this.competencyService.getAllForCourse(this.courseId).subscribe({ - next: (res) => { - if (res.body) { - this.disabledIds = res.body.map((competency) => competency.id).filter((id): id is number => !!id); - } - }, - error: (error: HttpErrorResponse) => onError(this.alertService, error), - }); + this.courseId = Number(this.activatedRoute.snapshot.paramMap.get('courseId')); + //load competencies and prerequisites of this course to disable their import buttons + const competencySubscription = this.competencyService.getAllForCourse(this.courseId); + const prerequisiteSubscription = this.prerequisiteService.getAllPrerequisitesForCourse(this.courseId); + forkJoin([competencySubscription, prerequisiteSubscription]).subscribe({ + next: ([competenciesResponse, prerequisites]) => { + const competencies = competenciesResponse.body ?? []; + const competencyIds = competencies.map((competency) => competency.id).filter((id): id is number => !!id); + // do not allow import of competencies that are already imported as prerequisites + const referencedIds = prerequisites.map((prerequisite) => prerequisite.linkedCourseCompetency?.id).filter((id): id is number => !!id); + this.disabledIds = [...competencyIds, ...referencedIds]; + this.performSearch(); + }, + error: (error: HttpErrorResponse) => onError(this.alertService, error), }); } diff --git a/src/main/webapp/app/course/competencies/import-competencies/import-prerequisites.component.ts b/src/main/webapp/app/course/competencies/import-competencies/import-prerequisites.component.ts index afaa2421b716..4c4d155c71b3 100644 --- a/src/main/webapp/app/course/competencies/import-competencies/import-prerequisites.component.ts +++ b/src/main/webapp/app/course/competencies/import-competencies/import-prerequisites.component.ts @@ -1,8 +1,7 @@ import { HttpErrorResponse } from '@angular/common/http'; -import { Component, inject } from '@angular/core'; +import { Component } from '@angular/core'; import { ImportCourseCompetenciesComponent } from 'app/course/competencies/import-competencies/import-course-competencies.component'; import { onError } from 'app/shared/util/global.utils'; -import { PrerequisiteService } from '../prerequisite.service'; @Component({ selector: 'jhi-import-prerequisites', @@ -12,8 +11,6 @@ export class ImportPrerequisitesComponent extends ImportCourseCompetenciesCompon entityType = 'prerequisite'; allowRelationImport = false; - private readonly prerequisiteService: PrerequisiteService = inject(PrerequisiteService); - onSubmit() { const idsToImport = this.selectedCourseCompetencies.resultsOnPage.map((c) => c.id).filter((c): c is number => c !== undefined); this.prerequisiteService.importPrerequisites(idsToImport, this.courseId).subscribe({ diff --git a/src/test/javascript/spec/component/competencies/import/import-course-competencies.component.spec.ts b/src/test/javascript/spec/component/competencies/import/import-course-competencies.component.spec.ts index 94414e928dda..70e5a1ad213c 100644 --- a/src/test/javascript/spec/component/competencies/import/import-course-competencies.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/import/import-course-competencies.component.spec.ts @@ -3,9 +3,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MockProvider } from 'ng-mocks'; import { ImportCourseCompetenciesComponent } from 'app/course/competencies/import-competencies/import-course-competencies.component'; import { FormsModule } from 'app/forms/forms.module'; -import { MockActivatedRoute } from '../../../helpers/mocks/activated-route/mock-activated-route'; import { MockRouter } from '../../../helpers/mocks/mock-router'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute, Router, convertToParamMap } from '@angular/router'; import { CompetencyService } from 'app/course/competencies/competency.service'; import { of } from 'rxjs'; import { Competency } from 'app/entities/competency.model'; @@ -13,6 +12,8 @@ import { HttpResponse } from '@angular/common/http'; import { PageableSearch } from 'app/shared/table/pageable-table'; import { Component } from '@angular/core'; import { SortService } from 'app/shared/service/sort.service'; +import { PrerequisiteService } from 'app/course/competencies/prerequisite.service'; +import { Prerequisite } from 'app/entities/prerequisite.model'; @Component({ template: '' }) class DummyImportComponent extends ImportCourseCompetenciesComponent { @@ -25,6 +26,7 @@ describe('ImportCourseCompetenciesComponent', () => { let componentFixture: ComponentFixture; let component: DummyImportComponent; let competencyService: CompetencyService; + let prerequisiteService: PrerequisiteService; let getAllSpy: any; let getForImportSpy: any; @@ -35,10 +37,13 @@ describe('ImportCourseCompetenciesComponent', () => { providers: [ { provide: ActivatedRoute, - useValue: new MockActivatedRoute({ courseId: 1 }), + useValue: { + snapshot: { paramMap: convertToParamMap({ courseId: 1 }) }, + } as ActivatedRoute, }, { provide: Router, useClass: MockRouter }, MockProvider(CompetencyService), + MockProvider(PrerequisiteService), ], }) .compileComponents() @@ -46,6 +51,7 @@ describe('ImportCourseCompetenciesComponent', () => { componentFixture = TestBed.createComponent(DummyImportComponent); component = componentFixture.componentInstance; competencyService = TestBed.inject(CompetencyService); + prerequisiteService = TestBed.inject(PrerequisiteService); getAllSpy = jest.spyOn(competencyService, 'getAllForCourse'); getForImportSpy = jest.spyOn(competencyService, 'getForImport'); }); @@ -61,6 +67,12 @@ describe('ImportCourseCompetenciesComponent', () => { }); it('should initialize values correctly', () => { + jest.spyOn(prerequisiteService, 'getAllPrerequisitesForCourse').mockReturnValue( + of([ + { id: 3, linkedCourseCompetency: { id: 11 } }, + { id: 4, linkedCourseCompetency: { id: 12 } }, + ] as Prerequisite[]), + ); getAllSpy.mockReturnValue( of({ body: [{ id: 1 }, { id: 2 }], @@ -75,7 +87,8 @@ describe('ImportCourseCompetenciesComponent', () => { componentFixture.detectChanges(); - expect(component.disabledIds).toHaveLength(2); + expect(component.disabledIds).toHaveLength(4); + expect(component.disabledIds).toContainAllValues([1, 2, 11, 12]); expect(component.searchedCourseCompetencies.resultsOnPage).toHaveLength(3); }); From 00a1c19bcda2333d4ccaf1c563f1293b115dd56d Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Tue, 4 Jun 2024 18:06:37 +0200 Subject: [PATCH 47/78] Fix tests --- .../import/import-competencies.component.spec.ts | 10 +++++----- .../import/import-prerequisites.component.spec.ts | 11 +++++------ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/test/javascript/spec/component/competencies/import/import-competencies.component.spec.ts b/src/test/javascript/spec/component/competencies/import/import-competencies.component.spec.ts index 861889fb38b5..5f5d5074208b 100644 --- a/src/test/javascript/spec/component/competencies/import/import-competencies.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/import/import-competencies.component.spec.ts @@ -1,13 +1,12 @@ import { ArtemisTestModule } from '../../../test.module'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponent, MockPipe } from 'ng-mocks'; import { ImportCompetenciesComponent } from 'app/course/competencies/import-competencies/import-competencies.component'; import { ButtonComponent } from 'app/shared/components/button.component'; import { FormsModule } from 'app/forms/forms.module'; -import { MockActivatedRoute } from '../../../helpers/mocks/activated-route/mock-activated-route'; import { MockRouter } from '../../../helpers/mocks/mock-router'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute, Router, convertToParamMap } from '@angular/router'; import { CompetencyService } from 'app/course/competencies/competency.service'; import { of } from 'rxjs'; import { CompetencyWithTailRelationDTO } from 'app/entities/competency.model'; @@ -33,10 +32,11 @@ describe('ImportCompetenciesComponent', () => { providers: [ { provide: ActivatedRoute, - useValue: new MockActivatedRoute({ courseId: 1 }), + useValue: { + snapshot: { paramMap: convertToParamMap({ courseId: 1 }) }, + } as ActivatedRoute, }, { provide: Router, useClass: MockRouter }, - MockProvider(CompetencyService), ], }) .compileComponents() diff --git a/src/test/javascript/spec/component/competencies/import/import-prerequisites.component.spec.ts b/src/test/javascript/spec/component/competencies/import/import-prerequisites.component.spec.ts index 2f476bafd24a..021f0dfa3867 100644 --- a/src/test/javascript/spec/component/competencies/import/import-prerequisites.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/import/import-prerequisites.component.spec.ts @@ -1,14 +1,12 @@ import { ArtemisTestModule } from '../../../test.module'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponent, MockPipe } from 'ng-mocks'; import { ImportPrerequisitesComponent } from 'app/course/competencies/import-competencies/import-prerequisites.component'; import { ButtonComponent } from 'app/shared/components/button.component'; import { FormsModule } from 'app/forms/forms.module'; -import { MockActivatedRoute } from '../../../helpers/mocks/activated-route/mock-activated-route'; import { MockRouter } from '../../../helpers/mocks/mock-router'; -import { ActivatedRoute, Router } from '@angular/router'; -import { CompetencyService } from 'app/course/competencies/competency.service'; +import { ActivatedRoute, Router, convertToParamMap } from '@angular/router'; import { of } from 'rxjs'; import { ImportCompetenciesTableComponent } from 'app/course/competencies/import-competencies/import-competencies-table.component'; import { CompetencySearchComponent } from 'app/course/competencies/import-competencies/competency-search.component'; @@ -32,10 +30,11 @@ describe('ImportPrerequisitesComponent', () => { providers: [ { provide: ActivatedRoute, - useValue: new MockActivatedRoute({ courseId: 1 }), + useValue: { + snapshot: { paramMap: convertToParamMap({ courseId: 1 }) }, + } as ActivatedRoute, }, { provide: Router, useClass: MockRouter }, - MockProvider(CompetencyService), ], }) .compileComponents() From 3f0b74e8418101a25742614d847e0a42e4bb2efb Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Wed, 5 Jun 2024 16:50:49 +0200 Subject: [PATCH 48/78] Fix merge problems --- .../artemis/domain/competency/Competency.java | 7 +---- .../artemis/web/rest/CompetencyResource.java | 5 +--- .../course-competencies.component.ts | 20 ++++++------- .../course-competencies.component.spec.ts | 28 ------------------- 4 files changed, 10 insertions(+), 50 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 f606be6fe0ab..500ac223b164 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 @@ -47,11 +47,6 @@ public class Competency extends CourseCompetency { @JsonIgnoreProperties({ "competencies", "course" }) private Set learningPaths = new HashSet<>(); - @ManyToOne - @JoinColumn(name = "linked_standardized_competency_id") - @JsonIgnoreProperties({ "competencies" }) - private StandardizedCompetency linkedStandardizedCompetency; - @OneToMany(mappedBy = "competency", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) private Set competencyJols = new HashSet<>(); @@ -108,7 +103,7 @@ public void addLectureUnit(LectureUnit lectureUnit) { /** * Removes the lecture unit from the competency (bidirectional) - * Note: ExerciseUnits are not accepted, should be set via the connected exercise (see {@link #removeExercise(Exercise)}) + * Note: ExerciseUnits are not accepted, should be set via the connected exercise * * @param lectureUnit The lecture unit to remove */ diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java index 93251ebd5c7d..cf65e0298346 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java @@ -41,7 +41,6 @@ import de.tum.in.www1.artemis.repository.CompetencyRelationRepository; import de.tum.in.www1.artemis.repository.CompetencyRepository; 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.security.Role; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastEditor; @@ -56,7 +55,6 @@ 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.PrerequisiteService; 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; @@ -113,8 +111,7 @@ public CompetencyResource(CourseRepository courseRepository, AuthorizationCheckS CompetencyRepository competencyRepository, CompetencyRelationRepository competencyRelationRepository, CompetencyService competencyService, CompetencyProgressRepository competencyProgressRepository, CompetencyProgressService competencyProgressService, ExerciseService exerciseService, LectureUnitService lectureUnitService, CompetencyRelationService competencyRelationService, - Optional irisCompetencyGenerationSessionService, PrerequisiteService prerequisiteService, - PrerequisiteRepository prerequisiteRepository, CompetencyJolService competencyJolService) { + Optional irisCompetencyGenerationSessionService, CompetencyJolService competencyJolService) { this.courseRepository = courseRepository; this.competencyRelationRepository = competencyRelationRepository; this.authorizationCheckService = authorizationCheckService; diff --git a/src/main/webapp/app/overview/course-competencies/course-competencies.component.ts b/src/main/webapp/app/overview/course-competencies/course-competencies.component.ts index e8794e80805c..da95d05130f6 100644 --- a/src/main/webapp/app/overview/course-competencies/course-competencies.component.ts +++ b/src/main/webapp/app/overview/course-competencies/course-competencies.component.ts @@ -3,9 +3,9 @@ import { CompetencyService } from 'app/course/competencies/competency.service'; import { ActivatedRoute } from '@angular/router'; import { AlertService } from 'app/core/util/alert.service'; import { onError } from 'app/shared/util/global.utils'; -import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; +import { HttpErrorResponse } from '@angular/common/http'; import { Competency, CompetencyJol } from 'app/entities/competency.model'; -import { Observable, Subscription, forkJoin } from 'rxjs'; +import { Subscription, forkJoin, of } from 'rxjs'; import { Course } from 'app/entities/course.model'; import { faAngleDown, faAngleUp } from '@fortawesome/free-solid-svg-icons'; import { CourseStorageService } from 'app/course/manage/course-storage.service'; @@ -93,23 +93,19 @@ export class CourseCompetenciesComponent implements OnInit, OnDestroy { loadData() { this.isLoading = true; - const observables = [this.competencyService.getAllForCourse(this.courseId), this.prerequisiteService.getAllPrerequisitesForCourse(this.courseId)] as Observable< - HttpResponse - >[]; + const getAllCompetenciesObservable = this.competencyService.getAllForCourse(this.courseId); + const prerequisitesObservable = this.prerequisiteService.getAllPrerequisitesForCourse(this.courseId); + const competencyJolObservable = this.judgementOfLearningEnabled ? this.competencyService.getJoLAllForCourse(this.courseId) : of(undefined); - if (this.judgementOfLearningEnabled) { - observables.push(this.competencyService.getJoLAllForCourse(this.courseId)); - } - - forkJoin(observables).subscribe({ + forkJoin([getAllCompetenciesObservable, prerequisitesObservable, competencyJolObservable]).subscribe({ next: ([competencies, prerequisites, judgementOfLearningMap]) => { this.competencies = competencies.body! as Competency[]; this.prerequisites = prerequisites; - if (this.judgementOfLearningEnabled) { + if (judgementOfLearningMap !== undefined) { const competenciesMap: { [key: number]: Competency } = Object.fromEntries(this.competencies.map((competency) => [competency.id, competency])); this.judgementOfLearningMap = Object.fromEntries( - Object.entries((judgementOfLearningMap?.body ?? {}) as { [key: number]: { current: CompetencyJol; prior?: CompetencyJol } }).filter(([key, value]) => { + Object.entries((judgementOfLearningMap.body ?? {}) as { [key: number]: { current: CompetencyJol; prior?: CompetencyJol } }).filter(([key, value]) => { const progress = competenciesMap[Number(key)]?.userProgress?.first(); return value.current.competencyProgress === (progress?.progress ?? 0) && value.current.competencyConfidence === (progress?.confidence ?? 0); }), diff --git a/src/test/javascript/spec/component/competencies/course-competencies.component.spec.ts b/src/test/javascript/spec/component/competencies/course-competencies.component.spec.ts index ea65dd3f198e..06d79d2f3e3a 100644 --- a/src/test/javascript/spec/component/competencies/course-competencies.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/course-competencies.component.spec.ts @@ -94,34 +94,6 @@ describe('CourseCompetencies', () => { expect(courseCompetenciesComponent.courseId).toBe(1); }); - it('should load progress for each competency in a given course', () => { - const courseStorageService = TestBed.inject(CourseStorageService); - const competency: Competency = {}; - competency.userProgress = [{ progress: 70, confidence: 45 } as CompetencyProgress]; - const textUnit = new TextUnit(); - competency.id = 1; - competency.description = 'Petierunt uti sibi concilium totius'; - competency.lectureUnits = [textUnit]; - - // Mock a course that was already fetched in another component - const course = new Course(); - course.id = 1; - course.competencies = [competency]; - course.prerequisites = [competency]; - courseStorageService.setCourses([course]); - const getCourseStub = jest.spyOn(courseStorageService, 'getCourse').mockReturnValue(course); - - const getAllForCourseSpy = jest.spyOn(competencyService, 'getAllForCourse'); - - courseCompetenciesComponentFixture.detectChanges(); - - expect(getCourseStub).toHaveBeenCalledOnce(); - expect(getCourseStub).toHaveBeenCalledWith(1); - expect(courseCompetenciesComponent.course).toEqual(course); - expect(courseCompetenciesComponent.competencies).toEqual([competency]); - expect(getAllForCourseSpy).not.toHaveBeenCalled(); // do not load competencies again as already fetched - }); - it('should load prerequisites and competencies (with associated progress) and display a card for each of them', () => { const competency: Competency = {}; const textUnit = new TextUnit(); From f3d9dbdc05d384e6029453c279c0aca6701ad934 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Wed, 5 Jun 2024 16:55:56 +0200 Subject: [PATCH 49/78] Fix merge problems --- .../in/www1/artemis/service/competency/CompetencyService.java | 1 - 1 file changed, 1 deletion(-) 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 f1997329dfc5..ceeffe5f6ce3 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 @@ -37,7 +37,6 @@ import de.tum.in.www1.artemis.web.rest.dto.competency.CompetencyRelationDTO; import de.tum.in.www1.artemis.web.rest.dto.competency.CompetencyWithTailRelationDTO; import de.tum.in.www1.artemis.web.rest.dto.pageablesearch.CompetencyPageableSearchDTO; -import de.tum.in.www1.artemis.web.rest.dto.pageablesearch.SearchTermPageableSearchDTO; import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; import de.tum.in.www1.artemis.web.rest.util.PageUtil; From e3f4af046aaceea475ff91b8ea9cd6d5f96744d2 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Fri, 7 Jun 2024 13:11:54 +0200 Subject: [PATCH 50/78] Improve client coverage --- .../competencies/prerequisite.service.spec.ts | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts b/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts index 49cab6e80f95..3cbbb7016110 100644 --- a/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts +++ b/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts @@ -1,7 +1,7 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { TestBed, fakeAsync, tick } from '@angular/core/testing'; import { PrerequisiteService } from 'app/course/competencies/prerequisite.service'; -import { Prerequisite } from 'app/entities/prerequisite.model'; +import { Prerequisite, PrerequisiteResponseDTO } from 'app/entities/prerequisite.model'; describe('PrerequisiteService', () => { let prerequisiteService: PrerequisiteService; @@ -37,6 +37,17 @@ describe('PrerequisiteService', () => { expect(actualPrerequisites).toEqual(expectedPrerequisites); })); + it('should return empty array for no found prerequisites', fakeAsync(() => { + let actualPrerequisites: any; + prerequisiteService.getAllPrerequisitesForCourse(1).subscribe((resp) => (actualPrerequisites = resp)); + + const req = httpTestingController.expectOne({ method: 'GET' }); + req.flush(null); + tick(); + + expect(actualPrerequisites).toEqual([]); + })); + it('should import prerequisites', fakeAsync(() => { let actualPrerequisites: any; const expectedPrerequisites: Prerequisite[] = [ @@ -53,6 +64,17 @@ describe('PrerequisiteService', () => { expect(actualPrerequisites).toEqual(expectedPrerequisites); })); + it('should return empty array for no imported prerequisites', fakeAsync(() => { + let actualPrerequisites: any; + prerequisiteService.importPrerequisites([], 1).subscribe((resp) => (actualPrerequisites = resp)); + + const req = httpTestingController.expectOne({ method: 'POST' }); + req.flush(null); + tick(); + + expect(actualPrerequisites).toEqual([]); + })); + it('should remove a prerequisite', fakeAsync(() => { let result: any; prerequisiteService.deletePrerequisite(1, 1).subscribe((resp) => (result = resp.ok)); @@ -62,4 +84,22 @@ describe('PrerequisiteService', () => { expect(result).toBeTrue(); })); + + it('should convert response dto to to prerequisite', () => { + const expectedPrerequisite: Prerequisite = { id: 1, title: 'title1', linkedCourseCompetency: { id: 1, course: { id: 1, title: '', semester: 'SS01' } } }; + const prerequisiteDTO: PrerequisiteResponseDTO = { + id: 1, + title: 'title1', + linkedCourseCompetencyDTO: { + id: 1, + courseId: 1, + courseTitle: '', + semester: 'SS01', + }, + }; + + const actualPrerequisite = PrerequisiteService['convertResponseDTOToPrerequisite'](prerequisiteDTO); + + expect(actualPrerequisite).toEqual(expectedPrerequisite); + }); }); From 0437d5aba8b0b996eb2cbce8dc5bd9e900b44d6b Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Fri, 7 Jun 2024 14:00:48 +0200 Subject: [PATCH 51/78] Further improve client coverage --- .../create-competency.component.spec.ts | 19 +++++++++++++++++++ ...port-course-competencies.component.spec.ts | 13 +++++++++++++ 2 files changed, 32 insertions(+) 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 b9bf38de4a8d..a9e27cdee558 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 @@ -15,6 +15,8 @@ import { Competency } from 'app/entities/competency.model'; import { By } from '@angular/platform-browser'; import { CompetencyFormStubComponent } from './competency-form-stub.component'; import { DocumentationButtonComponent } from 'app/shared/components/documentation-button/documentation-button.component'; +import { Lecture } from 'app/entities/lecture.model'; +import { LectureUnitType } from 'app/entities/lecture-unit/lectureUnit.model'; describe('CreateCompetency', () => { let createCompetencyComponentFixture: ComponentFixture; @@ -65,6 +67,23 @@ describe('CreateCompetency', () => { expect(createCompetencyComponent).toBeDefined(); }); + it('should set lecture units', () => { + const lectureService = TestBed.inject(LectureService); + const lecture: Lecture = { + id: 1, + lectureUnits: [{ id: 1, type: LectureUnitType.TEXT }], + }; + const lecturesResponse = new HttpResponse({ + body: [lecture], + status: 200, + }); + jest.spyOn(lectureService, 'findAllByCourseId').mockReturnValue(of(lecturesResponse)); + + createCompetencyComponentFixture.detectChanges(); + + expect(createCompetencyComponent.lecturesWithLectureUnits).toEqual([lecture]); + }); + it('should send POST request upon form submission and navigate', () => { const router: Router = TestBed.inject(Router); const competencyService = TestBed.inject(CompetencyService); diff --git a/src/test/javascript/spec/component/competencies/import/import-course-competencies.component.spec.ts b/src/test/javascript/spec/component/competencies/import/import-course-competencies.component.spec.ts index 70e5a1ad213c..a10ab531af12 100644 --- a/src/test/javascript/spec/component/competencies/import/import-course-competencies.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/import/import-course-competencies.component.spec.ts @@ -178,4 +178,17 @@ describe('ImportCourseCompetenciesComponent', () => { expect(component.selectedCourseCompetencies.resultsOnPage).toHaveLength(3); expect(component.disabledIds).toHaveLength(3); }); + + it('should not unload with pending changes', () => { + const deactivateWarningSpy = jest.spyOn(component, 'canDeactivateWarning', 'get'); + + component['isSubmitted'] = true; + component.selectedCourseCompetencies = { resultsOnPage: [{ id: 1 }], numberOfPages: 0 }; + component['unloadNotification']({ returnValue: '' }); + expect(deactivateWarningSpy).not.toHaveBeenCalled(); + + component['isSubmitted'] = false; + component['unloadNotification']({ returnValue: '' }); + expect(deactivateWarningSpy).toHaveBeenCalled(); + }); }); From eb06aa98ba55d71ed40830aba46e6a8960ace515 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Wed, 5 Jun 2024 16:15:59 +0200 Subject: [PATCH 52/78] First version of create/edit --- .../CourseCompetencyRepository.java | 7 + .../artemis/web/rest/CompetencyResource.java | 19 ++- .../rest/competency/PrerequisiteResource.java | 18 ++ .../competency-management.component.html | 36 ++-- .../course/competencies/competency.service.ts | 6 + .../create-prerequisite.component.html | 14 ++ .../create-prerequisite.component.ts | 58 +++++++ .../edit-prerequisite.component.html | 14 ++ .../edit-prerequisite.component.ts | 70 ++++++++ .../prerequisite-form.component.html | 83 +++++++++ .../prerequisite-form.component.ts | 159 ++++++++++++++++++ .../competencies/prerequisite.service.ts | 14 +- .../course/manage/course-management.route.ts | 19 +++ .../webapp/app/entities/competency.model.ts | 2 + ...lecture-wizard-competencies.component.html | 4 +- src/main/webapp/i18n/de/competency.json | 7 +- src/main/webapp/i18n/en/competency.json | 7 +- .../lecture/PrerequisiteIntegrationTest.java | 30 ++++ .../competencies/prerequisite.service.spec.ts | 2 +- 19 files changed, 546 insertions(+), 23 deletions(-) create mode 100644 src/main/webapp/app/course/competencies/prerequisite-form/create-prerequisite.component.html create mode 100644 src/main/webapp/app/course/competencies/prerequisite-form/create-prerequisite.component.ts create mode 100644 src/main/webapp/app/course/competencies/prerequisite-form/edit-prerequisite.component.html create mode 100644 src/main/webapp/app/course/competencies/prerequisite-form/edit-prerequisite.component.ts create mode 100644 src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.html create mode 100644 src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.ts 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 575b930cfe81..5b35d63ab1e8 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 @@ -22,6 +22,13 @@ public interface CourseCompetencyRepository extends JpaRepository findAllByIdAndUserIsAtLeastEditorInCourse(@Param("courseCompetencyIds") List courseCompetencyIds, @Param("groups") Set groups); + @Query(""" + SELECT c.title + FROM CourseCompetency c + WHERE c.course.id = :courseId + """) + List findAllTitlesByCourseId(@Param("courseId") long courseId); + /** * 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} diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java index b3f3ded99636..f6483d6009d6 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java @@ -39,6 +39,7 @@ 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.PrerequisiteRepository; import de.tum.in.www1.artemis.repository.UserRepository; @@ -102,12 +103,14 @@ public class CompetencyResource { private final CompetencyRelationService competencyRelationService; + private final CourseCompetencyRepository courseCompetencyRepository; + 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, PrerequisiteService prerequisiteService, - PrerequisiteRepository prerequisiteRepository) { + PrerequisiteRepository prerequisiteRepository, CourseCompetencyRepository courseCompetencyRepository) { this.courseRepository = courseRepository; this.competencyRelationRepository = competencyRelationRepository; this.authorizationCheckService = authorizationCheckService; @@ -120,6 +123,7 @@ public CompetencyResource(CourseRepository courseRepository, AuthorizationCheckS this.lectureUnitService = lectureUnitService; this.competencyRelationService = competencyRelationService; this.irisCompetencyGenerationSessionService = irisCompetencyGenerationSessionService; + this.courseCompetencyRepository = courseCompetencyRepository; } /** @@ -532,6 +536,19 @@ public ResponseEntity> generateCompetenciesFromCourseDescriptio return ResponseEntity.ok().body(competencies); } + /** + * GET courses/{courseId}/competencies/titles : Returns the titles of all course competencies. Used for a validator in the client + * + * @param courseId the id of the current course + * @return the titles of all course competencies + */ + @GetMapping("courses/{courseId}/competencies/titles") + @EnforceAtLeastEditorInCourse + public ResponseEntity> getCourseCompetencyTitles(@PathVariable Long courseId) { + final var titles = courseCompetencyRepository.findAllTitlesByCourseId(courseId); + return ResponseEntity.ok(titles); + } + /** * Checks if the user has the necessary permissions and the competency matches the course. * 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 64dabc6ce2be..d43526c93a34 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 @@ -25,6 +25,7 @@ import de.tum.in.www1.artemis.security.Role; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent; 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.PrerequisiteService; import de.tum.in.www1.artemis.web.rest.dto.competency.PrerequisiteRequestDTO; @@ -79,6 +80,23 @@ public ResponseEntity> getPrerequisites(@PathVaria return ResponseEntity.ok(prerequisites.stream().map(PrerequisiteResponseDTO::of).toList()); } + /** + * GET /courses/:courseId/competencies/prerequisites/:prerequisiteId : Gets the prerequisite competency with the given id for a course. + * + * @param prerequisiteId the id of the prerequisite to fetch + * @param courseId the id of the course in which the prerequisite should exist + * @return the ResponseEntity with status 200 (OK) and with body the found prerequisite or with status 404 (Not Found) + */ + @GetMapping("courses/{courseId}/competencies/prerequisites/{prerequisiteId}") + @EnforceAtLeastStudentInCourse + public ResponseEntity getPrerequisite(@PathVariable long prerequisiteId, @PathVariable long courseId) { + log.debug("REST request to get prerequisite with id: {}", prerequisiteId); + + var prerequisite = prerequisiteRepository.findByIdAndCourseIdElseThrow(prerequisiteId, courseId); + + return ResponseEntity.ok(PrerequisiteResponseDTO.of(prerequisite)); + } + /** * POST /courses/:courseId/prerequisites : creates a new prerequisite competency. * 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 5e9be84f6c88..00ab1fd9beff 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 @@ -145,9 +145,12 @@

+ + + +
-
@if (prerequisites.length) { @@ -192,17 +195,26 @@

} - } diff --git a/src/main/webapp/app/course/competencies/competency.service.ts b/src/main/webapp/app/course/competencies/competency.service.ts index 495d76a3ad58..01ceb8965fea 100644 --- a/src/main/webapp/app/course/competencies/competency.service.ts +++ b/src/main/webapp/app/course/competencies/competency.service.ts @@ -142,6 +142,12 @@ export class CompetencyService { }); } + getCourseCompetencyTitles(courseId: number) { + return this.httpClient.get(`${this.resourceURL}/courses/${courseId}/competencies/titles`, { + observe: 'response', + }); + } + //helper methods /** diff --git a/src/main/webapp/app/course/competencies/prerequisite-form/create-prerequisite.component.html b/src/main/webapp/app/course/competencies/prerequisite-form/create-prerequisite.component.html new file mode 100644 index 000000000000..be5e5823bc88 --- /dev/null +++ b/src/main/webapp/app/course/competencies/prerequisite-form/create-prerequisite.component.html @@ -0,0 +1,14 @@ +@if (isLoading) { +
+
+ +
+
+} @else { +
+
+

+
+ +
+} diff --git a/src/main/webapp/app/course/competencies/prerequisite-form/create-prerequisite.component.ts b/src/main/webapp/app/course/competencies/prerequisite-form/create-prerequisite.component.ts new file mode 100644 index 000000000000..dd80eac44d87 --- /dev/null +++ b/src/main/webapp/app/course/competencies/prerequisite-form/create-prerequisite.component.ts @@ -0,0 +1,58 @@ +import { Component, OnInit } from '@angular/core'; +import { onError } from 'app/shared/util/global.utils'; +import { ActivatedRoute, Router } from '@angular/router'; +import { AlertService } from 'app/core/util/alert.service'; +import { finalize } from 'rxjs/operators'; +import { HttpErrorResponse } from '@angular/common/http'; +import { Prerequisite } from 'app/entities/prerequisite.model'; +import { PrerequisiteService } from 'app/course/competencies/prerequisite.service'; +import { PrerequisiteFormComponent } from 'app/course/competencies/prerequisite-form/prerequisite-form.component'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; + +@Component({ + selector: 'jhi-create-prerequisite', + templateUrl: './create-prerequisite.component.html', + standalone: true, + styles: [], + imports: [PrerequisiteFormComponent, ArtemisSharedModule], +}) +export class CreatePrerequisiteComponent implements OnInit { + isLoading: boolean; + courseId: number; + + constructor( + private activatedRoute: ActivatedRoute, + private router: Router, + private alertService: AlertService, + private prerequisiteService: PrerequisiteService, + ) {} + + ngOnInit(): void { + this.isLoading = true; + this.activatedRoute.params.subscribe((params) => { + this.courseId = params['courseId']; + this.isLoading = false; + }); + } + + createPrerequisite(prerequisite: Prerequisite) { + this.isLoading = true; + this.prerequisiteService + .createPrerequisite(prerequisite, this.courseId) + .pipe( + finalize(() => { + this.isLoading = false; + }), + ) + .subscribe({ + next: () => { + this.router.navigate(['../../'], { relativeTo: this.activatedRoute }); + }, + error: (res: HttpErrorResponse) => onError(this.alertService, res), + }); + } + + cancel() { + this.router.navigate(['../../'], { relativeTo: this.activatedRoute }); + } +} diff --git a/src/main/webapp/app/course/competencies/prerequisite-form/edit-prerequisite.component.html b/src/main/webapp/app/course/competencies/prerequisite-form/edit-prerequisite.component.html new file mode 100644 index 000000000000..0f76144ef213 --- /dev/null +++ b/src/main/webapp/app/course/competencies/prerequisite-form/edit-prerequisite.component.html @@ -0,0 +1,14 @@ +@if (isLoading) { +
+
+ +
+
+} @else { +
+
+

+
+ +
+} diff --git a/src/main/webapp/app/course/competencies/prerequisite-form/edit-prerequisite.component.ts b/src/main/webapp/app/course/competencies/prerequisite-form/edit-prerequisite.component.ts new file mode 100644 index 000000000000..02f08ba984fd --- /dev/null +++ b/src/main/webapp/app/course/competencies/prerequisite-form/edit-prerequisite.component.ts @@ -0,0 +1,70 @@ +import { Component, OnInit } from '@angular/core'; +import { onError } from 'app/shared/util/global.utils'; +import { ActivatedRoute, Router } from '@angular/router'; +import { AlertService } from 'app/core/util/alert.service'; +import { finalize, switchMap } from 'rxjs/operators'; +import { HttpErrorResponse } from '@angular/common/http'; +import { Prerequisite } from 'app/entities/prerequisite.model'; +import { PrerequisiteService } from 'app/course/competencies/prerequisite.service'; +import { PrerequisiteFormComponent } from 'app/course/competencies/prerequisite-form/prerequisite-form.component'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; + +@Component({ + selector: 'jhi-edit-prerequisite', + templateUrl: './edit-prerequisite.component.html', + standalone: true, + styles: [], + imports: [PrerequisiteFormComponent, ArtemisSharedModule], +}) +export class EditPrerequisiteComponent implements OnInit { + isLoading: boolean; + courseId: number; + existingPrerequisite: Prerequisite; + + constructor( + private activatedRoute: ActivatedRoute, + private router: Router, + private alertService: AlertService, + private prerequisiteService: PrerequisiteService, + ) {} + + ngOnInit(): void { + this.isLoading = true; + this.activatedRoute.params + .pipe( + switchMap((params) => { + console.log(params); + const prerequisiteId = Number(params['prerequisiteId']); + this.courseId = params['courseId']; + return this.prerequisiteService.getPrerequisite(prerequisiteId, this.courseId); + }), + ) + .subscribe({ + next: (prerequisite) => { + this.existingPrerequisite = prerequisite; + }, + error: (res: HttpErrorResponse) => onError(this.alertService, res), + }); + } + + updatePrerequisite(prerequisite: Prerequisite) { + this.isLoading = true; + this.prerequisiteService + .updatePrerequisite(prerequisite, this.existingPrerequisite.id!, this.courseId) + .pipe( + finalize(() => { + this.isLoading = false; + }), + ) + .subscribe({ + next: () => { + this.router.navigate(['../../'], { relativeTo: this.activatedRoute }); + }, + error: (res: HttpErrorResponse) => onError(this.alertService, res), + }); + } + + cancel() { + this.router.navigate(['../../'], { relativeTo: this.activatedRoute }); + } +} diff --git a/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.html b/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.html new file mode 100644 index 000000000000..1d08dc5dca34 --- /dev/null +++ b/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.html @@ -0,0 +1,83 @@ +
+ @if (form) { +
+
+ + + @if (form.controls.title.invalid && (form.controls.title.dirty || form.controls.title.touched)) { +
+ @if (form.controls.title.errors?.required) { + + } + @if (form.controls.title.errors?.maxlength) { + + } + @if (form.controls.title.errors?.titleUnique) { + + } +
+ } +
+
+ + + @if (form.controls.description.invalid && form.controls.description.dirty) { +
+ +
+ } +
+
+ +
+
+ + +
+
+ + + +
+
+ + + +
+
+ + +
+ + } +
diff --git a/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.ts b/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.ts new file mode 100644 index 000000000000..d0e7d2904395 --- /dev/null +++ b/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.ts @@ -0,0 +1,159 @@ +import { Component, EventEmitter, Input, OnInit, Output, inject } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { CompetencyService } from 'app/course/competencies/competency.service'; +import { merge, of } from 'rxjs'; +import { catchError, delay, map, switchMap } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; +import { CompetencyTaxonomy, CompetencyValidators, DEFAULT_MASTERY_THRESHOLD } from 'app/entities/competency.model'; +import { faBan, faQuestionCircle, faSave, faTimes } from '@fortawesome/free-solid-svg-icons'; +import dayjs from 'dayjs/esm'; +import { ButtonSize, ButtonType } from 'app/shared/components/button.component'; +import { Prerequisite } from 'app/entities/prerequisite.model'; +import { ArtemisCompetenciesModule } from 'app/course/competencies/competency.module'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; +import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; +import { FormDateTimePickerModule } from 'app/shared/date-time-picker/date-time-picker.module'; +import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; + +/** + * Async Validator to make sure that a competency title is unique within a course + */ +export const titleUniqueValidator = (competencyService: CompetencyService, courseId: number, initialTitle?: string) => { + return (competencyTitleControl: FormControl) => { + return of(competencyTitleControl.value).pipe( + delay(250), + switchMap((title) => { + if (initialTitle && title === initialTitle) { + return of(null); + } + return competencyService.getCourseCompetencyTitles(courseId).pipe( + map((res) => { + let competencyTitles: string[] = []; + if (res.body) { + competencyTitles = res.body; + } + if (title && competencyTitles.includes(title)) { + return { + titleUnique: { valid: false }, + }; + } else { + return null; + } + }), + catchError(() => of(null)), + ); + }), + ); + }; +}; + +@Component({ + selector: 'jhi-prerequisite-form', + templateUrl: './prerequisite-form.component.html', + imports: [ + ArtemisCompetenciesModule, + ArtemisSharedCommonModule, + ArtemisSharedComponentModule, + FormDateTimePickerModule, + ArtemisMarkdownEditorModule, + FontAwesomeModule, + ReactiveFormsModule, + ], + standalone: true, +}) +export class PrerequisiteFormComponent implements OnInit { + @Input() + prerequisite?: Prerequisite; + @Input() + courseId: number; + + @Output() + onCancel: EventEmitter = new EventEmitter(); + @Output() + onSubmit: EventEmitter = new EventEmitter(); + + protected form: FormGroup<{ + title: FormControl; + description: FormControl; + taxonomy: FormControl; + softDueDate: FormControl; + masteryThreshold: FormControl; + optional: FormControl; + }>; + protected suggestedTaxonomies: string[] = []; + private titleUniqueValidator = titleUniqueValidator; + + // Icons + protected readonly faTimes = faTimes; + protected readonly faQuestionCircle = faQuestionCircle; + protected readonly faBan = faBan; + protected readonly faSave = faSave; + // Constants + protected readonly competencyTaxonomy = CompetencyTaxonomy; + protected readonly competencyValidators = CompetencyValidators; + protected readonly ButtonSize = ButtonSize; + protected readonly ButtonType = ButtonType; + // Services + protected readonly formBuilder = inject(FormBuilder); + protected readonly competencyService = inject(CompetencyService); + protected readonly translateService = inject(TranslateService); + + ngOnInit(): void { + this.form = this.formBuilder.nonNullable.group({ + title: [ + this.prerequisite?.title, + [Validators.required, Validators.maxLength(CompetencyValidators.TITLE_MAX)], + [this.titleUniqueValidator(this.competencyService, this.courseId, this.prerequisite?.title)], + ], + description: [this.prerequisite?.description, [Validators.maxLength(CompetencyValidators.DESCRIPTION_MAX)]], + taxonomy: [this.prerequisite?.taxonomy], + softDueDate: [this.prerequisite?.softDueDate], + masteryThreshold: [ + this.prerequisite?.masteryThreshold ?? DEFAULT_MASTERY_THRESHOLD, + [Validators.min(CompetencyValidators.MASTERY_THRESHOLD_MIN), Validators.max(CompetencyValidators.MASTERY_THRESHOLD_MAX)], + ], + optional: [this.prerequisite?.optional ?? false], + }); + + merge(this.form.controls.title.valueChanges, this.form.controls.description.valueChanges).subscribe(() => this.suggestTaxonomies()); + } + + /** + * Updates description form on markdown change + * @param content markdown content + */ + updateDescriptionControl(content: string) { + this.form.controls.description.setValue(content); + this.form.controls.description.markAsDirty(); + } + + cancel() { + this.onCancel.emit(); + } + + submit() { + const updatedValues = this.form.getRawValue(); + const updatedPrerequisite: Prerequisite = { ...this.prerequisite, ...updatedValues }; + + this.onSubmit.emit(updatedPrerequisite); + } + + /** + * Suggest some taxonomies based on keywords used in the title or description. + * Triggered after the user changes the title or description input field. + */ + suggestTaxonomies() { + this.suggestedTaxonomies = []; + const title = this.form.controls.title?.value?.toLowerCase() ?? ''; + const description = this.form.controls.description?.value?.toLowerCase() ?? ''; + for (const taxonomy in this.competencyTaxonomy) { + const keywords = this.translateService.instant('artemisApp.competency.keywords.' + taxonomy).split(', '); + const taxonomyName = this.translateService.instant('artemisApp.competency.taxonomies.' + taxonomy); + keywords.push(taxonomyName); + if (keywords.map((keyword: string) => keyword.toLowerCase()).some((keyword: string) => title.includes(keyword) || description.includes(keyword))) { + this.suggestedTaxonomies.push(taxonomyName); + } + } + } +} diff --git a/src/main/webapp/app/course/competencies/prerequisite.service.ts b/src/main/webapp/app/course/competencies/prerequisite.service.ts index fadf0c1428c5..e9a0cbd0b14e 100644 --- a/src/main/webapp/app/course/competencies/prerequisite.service.ts +++ b/src/main/webapp/app/course/competencies/prerequisite.service.ts @@ -25,18 +25,24 @@ export class PrerequisiteService { //TODO: send title to entityTitleService when we allow prerequisite detail view. } - createPrerequisite(prerequisite: Prerequisite, prerequisiteId: number, courseId: number): Observable { + getPrerequisite(prerequisiteId: number, courseId: number) { + return this.httpClient + .get(`${this.resourceURL}/courses/${courseId}/competencies/prerequisites/${prerequisiteId}`, { observe: 'response' }) + .pipe(map((resp) => PrerequisiteService.convertResponseDTOToPrerequisite(resp.body!))); + } + + createPrerequisite(prerequisite: Prerequisite, courseId: number): Observable { const prerequisiteDTO = this.convertToRequestDTO(prerequisite); return this.httpClient - .post(`${this.resourceURL}/courses/${courseId}/competencies/prerequisites/${prerequisiteId}`, prerequisiteDTO, { observe: 'response' }) - .pipe(map((resp) => (resp.body ? this.convertResponseDTOToPrerequisite(resp.body) : undefined))); + .post(`${this.resourceURL}/courses/${courseId}/competencies/prerequisites`, prerequisiteDTO, { observe: 'response' }) + .pipe(map((resp) => (resp.body ? PrerequisiteService.convertResponseDTOToPrerequisite(resp.body) : undefined))); } updatePrerequisite(prerequisite: Prerequisite, prerequisiteId: number, courseId: number): Observable { const prerequisiteDTO = this.convertToRequestDTO(prerequisite); return this.httpClient .post(`${this.resourceURL}/courses/${courseId}/competencies/prerequisites/${prerequisiteId}`, prerequisiteDTO, { observe: 'response' }) - .pipe(map((resp) => (resp.body ? this.convertResponseDTOToPrerequisite(resp.body) : undefined))); + .pipe(map((resp) => (resp.body ? PrerequisiteService.convertResponseDTOToPrerequisite(resp.body) : undefined))); } deletePrerequisite(prerequisiteId: number, courseId: number) { diff --git a/src/main/webapp/app/course/manage/course-management.route.ts b/src/main/webapp/app/course/manage/course-management.route.ts index d267c090bb3b..e47ab7ed772a 100644 --- a/src/main/webapp/app/course/manage/course-management.route.ts +++ b/src/main/webapp/app/course/manage/course-management.route.ts @@ -260,6 +260,7 @@ export const courseManagementState: Routes = [ canActivate: [UserRouteAccessService, IrisGuard], canDeactivate: [PendingChangesGuard], }, + //TODO: move to own child route. { path: 'import-prerequisites', component: ImportPrerequisitesComponent, @@ -270,6 +271,24 @@ export const courseManagementState: Routes = [ canActivate: [UserRouteAccessService], canDeactivate: [PendingChangesGuard], }, + { + path: 'prerequisites/create', + loadComponent: () => import('app/course/competencies/prerequisite-form/create-prerequisite.component').then((m) => m.CreatePrerequisiteComponent), + data: { + authorities: [Authority.INSTRUCTOR, Authority.ADMIN], + pageTitle: 'artemisApp.prerequisite.create.title', + }, + canActivate: [UserRouteAccessService], + }, + { + path: 'prerequisites/:prerequisiteId/edit', + loadComponent: () => import('app/course/competencies/prerequisite-form/edit-prerequisite.component').then((m) => m.EditPrerequisiteComponent), + data: { + authorities: [Authority.INSTRUCTOR, Authority.ADMIN], + pageTitle: 'artemisApp.prerequisite.edit.title', + }, + canActivate: [UserRouteAccessService], + }, ], }, { diff --git a/src/main/webapp/app/entities/competency.model.ts b/src/main/webapp/app/entities/competency.model.ts index a33f4ed4e8e9..03985ae50d3d 100644 --- a/src/main/webapp/app/entities/competency.model.ts +++ b/src/main/webapp/app/entities/competency.model.ts @@ -35,6 +35,8 @@ export enum CompetencyRelationError { export enum CompetencyValidators { TITLE_MAX = 255, DESCRIPTION_MAX = 10000, + MASTERY_THRESHOLD_MIN = 0, + MASTERY_THRESHOLD_MAX = 100, } export const DEFAULT_MASTERY_THRESHOLD = 100; diff --git a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-competencies.component.html b/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-competencies.component.html index 280ec2536100..c80fe9e727cc 100644 --- a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-competencies.component.html +++ b/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-competencies.component.html @@ -1,9 +1,7 @@

- Make it easily visible what knowledge students will achieve when completing the units of this lecture by connecting them to competencies. +

diff --git a/src/main/webapp/i18n/de/competency.json b/src/main/webapp/i18n/de/competency.json index 8ddf3723caac..4ab9f5058a13 100644 --- a/src/main/webapp/i18n/de/competency.json +++ b/src/main/webapp/i18n/de/competency.json @@ -119,7 +119,6 @@ "titleRequiredValidationError": "Eine Kompetenz braucht einen Titel", "titleMaxLengthValidationError": "Der Titel einer Kompetenz ist auf maximal {{max}} Symbole beschränkt", "titleUniqueValidationError": "Es gibt bereits eine Kompetenz mit diesem Titel in dem Kurs", - "descriptionPlaceholder": "Was sollen die Studierenden lernen? Beispiel: \"Du kannst den Unterschied zwischen Rekursion und Schleifen erklären.\"", "descriptionMaxLengthValidationError": "Die Beschreibung der Kompetenz ist auf maximal {{max}} Symbole beschränkt", "softDueDate": "Empfohlenes Fertigstellungsdatum", "softDueDateHint": "Empfehle einen Zeitpunkt zu dem die Studierenden diese Kompetenz gemeistert haben sollten. Dies ist keine harte Frist.", @@ -206,6 +205,12 @@ "success": "{{ numPrerequisites }} Voraussetzungen importiert.", "selectedTableHeader": "Ausgewählte Kompetenzen", "selectedTableEmpty": "Keine Kompetenzen zum Import als Voraussetzungen ausgewählt" + }, + "create": { + "title": "Erstelle eine neue Voraussetzung" + }, + "edit": { + "title": "Bearbeite eine Voraussetzung" } }, "learningPath": { diff --git a/src/main/webapp/i18n/en/competency.json b/src/main/webapp/i18n/en/competency.json index c5425a6a1961..501423c8e660 100644 --- a/src/main/webapp/i18n/en/competency.json +++ b/src/main/webapp/i18n/en/competency.json @@ -119,7 +119,6 @@ "titleRequiredValidationError": "A competency needs to have a title", "titleMaxLengthValidationError": "A competency title is limited to {{max}} characters", "titleUniqueValidationError": "There already exists a competency with this title in the course", - "descriptionPlaceholder": "What do you expect students to learn? Example: \"You can explain the difference between call-by-value and call-by-reference.\"", "descriptionMaxLengthValidationError": "A competency description is limited to {{max}} characters", "softDueDate": "Recommended date of completion", "softDueDateHint": "Guide students by selecting when you expect them to have mastered this competency. This is not a hard deadline.", @@ -206,6 +205,12 @@ "success": "Imported {{ numPrerequisites }} prerequisites.", "selectedTableHeader": "Selected Prerequisites", "selectedTableEmpty": "No competencies selected to import as prerequisites" + }, + "create": { + "title": "Create a new Prerequisite" + }, + "edit": { + "title": "Edit a Prerequisite" } }, "learningPath": { 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 b1dd5518c5b7..6de28d58d792 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 @@ -105,6 +105,36 @@ void shouldNotReturnPrerequisitesForStudentNotInCourse() throws Exception { } } + @Nested + class GetPrerequisite { + + private static String url(long courseId, long prerequisiteId) { + return PrerequisiteIntegrationTest.baseUrl(courseId) + "/" + prerequisiteId; + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void shouldReturnPrerequisite() throws Exception { + var prerequisite = prerequisiteUtilService.createPrerequisite(course); + + PrerequisiteResponseDTO actualPrerequisite = request.get(url(prerequisite.getId(), course.getId()), HttpStatus.OK, PrerequisiteResponseDTO.class); + + assertThat(actualPrerequisite).isEqualTo(PrerequisiteResponseDTO.of(prerequisite)); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student42", roles = "USER") + void shouldReturnNotFoundIfCompetencyDoesNotExistInCourse() throws Exception { + var prerequisite = prerequisiteUtilService.createPrerequisite(course); + + // this competency does not exist in course2 + request.get(url(prerequisite.getId(), course2.getId()), HttpStatus.NOT_FOUND, PrerequisiteResponseDTO.class); + + // this competency does not exist at all + request.get(url(-1000L, course.getId()), HttpStatus.NOT_FOUND, PrerequisiteResponseDTO.class); + } + } + @Nested class CreatePrerequisite { diff --git a/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts b/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts index ae828e32941f..ae987716aad7 100644 --- a/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts +++ b/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts @@ -68,7 +68,7 @@ describe('PrerequisiteService', () => { const expectedPrerequisite: Prerequisite = { id: 1, title: 'newTitle', description: 'newDescription' }; const returnedFromService: Prerequisite = { ...expectedPrerequisite }; - prerequisiteService.createPrerequisite({ title: 'newTitle', description: 'newDescription' }, 1, 1).subscribe((resp) => (actualPrerequisite = resp)); + prerequisiteService.createPrerequisite({ title: 'newTitle', description: 'newDescription' }, 1).subscribe((resp) => (actualPrerequisite = resp)); const req = httpTestingController.expectOne({ method: 'POST' }); req.flush(returnedFromService); tick(); From 9239893a4b07cd7108788b80913c9135114fab9b Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Fri, 7 Jun 2024 18:17:11 +0200 Subject: [PATCH 53/78] fix infinite loading bug --- .../competency-management.component.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 bc962718eb34..f9311ec09d43 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 @@ -144,7 +144,11 @@ export class CompetencyManagementComponent implements OnInit, OnDestroy { const prerequisitesObservable = this.prerequisiteService.getAllPrerequisitesForCourse(this.courseId); const competencyProgressObservable = this.competencyService.getAllForCourse(this.courseId).pipe( switchMap((res) => { - this.competencies = res.body!; + if (!res.body || res.body.length === 0) { + // return observable with empty array as an empty forkJoin never emits a value, causing infinite loading + return of([]); + } + this.competencies = res.body; const progressObservable = this.competencies.map((lg) => { return this.competencyService.getCourseProgress(lg.id!, this.courseId); From f673cfa0a93cfbaa579997737a10377cf5d41ab1 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Fri, 7 Jun 2024 18:22:12 +0200 Subject: [PATCH 54/78] Add missing import --- .../competency-management/competency-management.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f9311ec09d43..fda1b100bf53 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 @@ -14,7 +14,7 @@ import { import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { filter, map, switchMap } from 'rxjs/operators'; import { onError } from 'app/shared/util/global.utils'; -import { Subject, Subscription, forkJoin } from 'rxjs'; +import { Subject, Subscription, forkJoin, of } from 'rxjs'; import { faFileImport, faPencilAlt, faPlus, faRobot, faTrash } from '@fortawesome/free-solid-svg-icons'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { DocumentationType } from 'app/shared/components/documentation-button/documentation-button.component'; From 3610a14e3d958e6264853c71e649b78685aebc4a Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Sat, 8 Jun 2024 16:16:09 +0200 Subject: [PATCH 55/78] Code Review Suggestions --- .../competency-management.component.ts | 18 +++++++-------- .../competencies/prerequisite.service.ts | 20 ++++------------- .../competencies/prerequisite.service.spec.ts | 22 ------------------- 3 files changed, 13 insertions(+), 47 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 fda1b100bf53..79c0ab359aef 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 @@ -57,15 +57,15 @@ export class CompetencyManagementComponent implements OnInit, OnDestroy { readonly documentationType: DocumentationType = 'Competencies'; // Injected services - private activatedRoute: ActivatedRoute = inject(ActivatedRoute); - private competencyService: CompetencyService = inject(CompetencyService); - private prerequisiteService: PrerequisiteService = inject(PrerequisiteService); - private alertService: AlertService = inject(AlertService); - private modalService: NgbModal = inject(NgbModal); - private profileService: ProfileService = inject(ProfileService); - private irisSettingsService: IrisSettingsService = inject(IrisSettingsService); - private translateService: TranslateService = inject(TranslateService); - private featureToggleService: FeatureToggleService = inject(FeatureToggleService); + private readonly activatedRoute: ActivatedRoute = inject(ActivatedRoute); + private readonly competencyService: CompetencyService = inject(CompetencyService); + private readonly prerequisiteService: PrerequisiteService = inject(PrerequisiteService); + private readonly alertService: AlertService = inject(AlertService); + private readonly modalService: NgbModal = inject(NgbModal); + private readonly profileService: ProfileService = inject(ProfileService); + private readonly irisSettingsService: IrisSettingsService = inject(IrisSettingsService); + private readonly translateService: TranslateService = inject(TranslateService); + private readonly featureToggleService: FeatureToggleService = inject(FeatureToggleService); ngOnInit(): void { this.activatedRoute.parent!.params.subscribe((params) => { diff --git a/src/main/webapp/app/course/competencies/prerequisite.service.ts b/src/main/webapp/app/course/competencies/prerequisite.service.ts index 581a94960397..44d9d0722ec5 100644 --- a/src/main/webapp/app/course/competencies/prerequisite.service.ts +++ b/src/main/webapp/app/course/competencies/prerequisite.service.ts @@ -14,14 +14,9 @@ export class PrerequisiteService { constructor(private httpClient: HttpClient) {} getAllPrerequisitesForCourse(courseId: number): Observable { - return this.httpClient.get(`${this.resourceURL}/courses/${courseId}/competencies/prerequisites`, { observe: 'response' }).pipe( - map((resp) => { - if (!resp.body) { - return []; - } - return resp.body.map((prerequisiteDTO) => PrerequisiteService.convertResponseDTOToPrerequisite(prerequisiteDTO)); - }), - ); + return this.httpClient + .get(`${this.resourceURL}/courses/${courseId}/competencies/prerequisites`, { observe: 'response' }) + .pipe(map((resp) => resp.body!.map((prerequisiteDTO) => PrerequisiteService.convertResponseDTOToPrerequisite(prerequisiteDTO)))); //TODO: send title to entityTitleService when we allow prerequisite detail view. } @@ -32,14 +27,7 @@ export class PrerequisiteService { importPrerequisites(prerequisiteIds: number[], courseId: number): Observable { return this.httpClient .post(`${this.resourceURL}/courses/${courseId}/competencies/prerequisites/import`, prerequisiteIds, { observe: 'response' }) - .pipe( - map((resp) => { - if (!resp.body) { - return []; - } - return resp.body.map((prerequisiteDTO) => PrerequisiteService.convertResponseDTOToPrerequisite(prerequisiteDTO)); - }), - ); + .pipe(map((resp) => resp.body!.map((prerequisiteDTO) => PrerequisiteService.convertResponseDTOToPrerequisite(prerequisiteDTO)))); } /** diff --git a/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts b/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts index 3cbbb7016110..30c90e9aa0f1 100644 --- a/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts +++ b/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts @@ -37,17 +37,6 @@ describe('PrerequisiteService', () => { expect(actualPrerequisites).toEqual(expectedPrerequisites); })); - it('should return empty array for no found prerequisites', fakeAsync(() => { - let actualPrerequisites: any; - prerequisiteService.getAllPrerequisitesForCourse(1).subscribe((resp) => (actualPrerequisites = resp)); - - const req = httpTestingController.expectOne({ method: 'GET' }); - req.flush(null); - tick(); - - expect(actualPrerequisites).toEqual([]); - })); - it('should import prerequisites', fakeAsync(() => { let actualPrerequisites: any; const expectedPrerequisites: Prerequisite[] = [ @@ -64,17 +53,6 @@ describe('PrerequisiteService', () => { expect(actualPrerequisites).toEqual(expectedPrerequisites); })); - it('should return empty array for no imported prerequisites', fakeAsync(() => { - let actualPrerequisites: any; - prerequisiteService.importPrerequisites([], 1).subscribe((resp) => (actualPrerequisites = resp)); - - const req = httpTestingController.expectOne({ method: 'POST' }); - req.flush(null); - tick(); - - expect(actualPrerequisites).toEqual([]); - })); - it('should remove a prerequisite', fakeAsync(() => { let result: any; prerequisiteService.deletePrerequisite(1, 1).subscribe((resp) => (result = resp.ok)); From 448dfba2409b93dcef06d08c22d71fef3553ee4e Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Sat, 8 Jun 2024 17:31:17 +0200 Subject: [PATCH 56/78] Add more client tests ^^ --- .../create-competency.component.spec.ts | 31 +++++++++++++++++++ .../import-prerequisites.component.spec.ts | 14 +++++++-- .../competencies/prerequisite.service.spec.ts | 14 ++++++--- 3 files changed, 51 insertions(+), 8 deletions(-) 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 a9e27cdee558..b11fc438fb94 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 @@ -84,6 +84,21 @@ describe('CreateCompetency', () => { expect(createCompetencyComponent.lecturesWithLectureUnits).toEqual([lecture]); }); + it('should set empty array of lecture units if lecture has none', () => { + const lectureService = TestBed.inject(LectureService); + const lecture: Lecture = { id: 1, lectureUnits: undefined }; + const expectedLecture: Lecture = { id: 1, lectureUnits: [] }; + const lecturesResponse = new HttpResponse({ + body: [lecture], + status: 200, + }); + jest.spyOn(lectureService, 'findAllByCourseId').mockReturnValue(of(lecturesResponse)); + + createCompetencyComponentFixture.detectChanges(); + + expect(createCompetencyComponent.lecturesWithLectureUnits).toEqual([expectedLecture]); + }); + it('should send POST request upon form submission and navigate', () => { const router: Router = TestBed.inject(Router); const competencyService = TestBed.inject(CompetencyService); @@ -122,4 +137,20 @@ describe('CreateCompetency', () => { expect(navigateSpy).toHaveBeenCalledOnce(); }); }); + + it('should not create competency if title is missing', () => { + const competencyService = TestBed.inject(CompetencyService); + + const formData: CompetencyFormData = { + title: undefined, + description: 'Lorem Ipsum', + optional: true, + }; + + const createSpy = jest.spyOn(competencyService, 'create'); + + createCompetencyComponent.createCompetency(formData); + + expect(createSpy).not.toHaveBeenCalled(); + }); }); diff --git a/src/test/javascript/spec/component/competencies/import/import-prerequisites.component.spec.ts b/src/test/javascript/spec/component/competencies/import/import-prerequisites.component.spec.ts index 021f0dfa3867..5de20087b9d8 100644 --- a/src/test/javascript/spec/component/competencies/import/import-prerequisites.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/import/import-prerequisites.component.spec.ts @@ -55,10 +55,18 @@ describe('ImportPrerequisitesComponent', () => { }); it('should import prerequisites on submit', () => { + component.courseId = 1; + component.selectedCourseCompetencies = { + resultsOnPage: [ + { id: 1, title: 'competency1' }, + { id: 2, title: 'competency2' }, + ], + numberOfPages: 0, + }; const importBulkSpy = jest.spyOn(prerequisiteService, 'importPrerequisites').mockReturnValue( of([ - { id: 1, title: 'competency1' }, - { id: 1, title: 'competency2' }, + { id: 11, title: 'competency1' }, + { id: 12, title: 'competency2' }, ]), ); const router: Router = TestBed.inject(Router); @@ -66,7 +74,7 @@ describe('ImportPrerequisitesComponent', () => { component.onSubmit(); - expect(importBulkSpy).toHaveBeenCalled(); + expect(importBulkSpy).toHaveBeenCalledWith([1, 2], 1); expect(navigateSpy).toHaveBeenCalled(); }); }); diff --git a/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts b/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts index 30c90e9aa0f1..ebbf653c4dd5 100644 --- a/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts +++ b/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts @@ -63,9 +63,9 @@ describe('PrerequisiteService', () => { expect(result).toBeTrue(); })); - it('should convert response dto to to prerequisite', () => { - const expectedPrerequisite: Prerequisite = { id: 1, title: 'title1', linkedCourseCompetency: { id: 1, course: { id: 1, title: '', semester: 'SS01' } } }; - const prerequisiteDTO: PrerequisiteResponseDTO = { + it('should convert response dtos to to prerequisite', () => { + const expectedPrerequisite1: Prerequisite = { id: 1, title: 'title1', linkedCourseCompetency: { id: 1, course: { id: 1, title: '', semester: 'SS01' } } }; + const prerequisiteDTO1: PrerequisiteResponseDTO = { id: 1, title: 'title1', linkedCourseCompetencyDTO: { @@ -75,9 +75,13 @@ describe('PrerequisiteService', () => { semester: 'SS01', }, }; + const expectedPrerequisite2: Prerequisite = { id: 2, title: 'title2' }; + const prerequisiteDTO2: PrerequisiteResponseDTO = { id: 2, title: 'title2' }; - const actualPrerequisite = PrerequisiteService['convertResponseDTOToPrerequisite'](prerequisiteDTO); + const actualPrerequisite1 = PrerequisiteService['convertResponseDTOToPrerequisite'](prerequisiteDTO1); + const actualPrerequisite2 = PrerequisiteService['convertResponseDTOToPrerequisite'](prerequisiteDTO2); - expect(actualPrerequisite).toEqual(expectedPrerequisite); + expect(actualPrerequisite1).toEqual(expectedPrerequisite1); + expect(actualPrerequisite2).toEqual(expectedPrerequisite2); }); }); From 661caf011c5b73070328d8e6bc59502c2572875b Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Sun, 9 Jun 2024 12:36:45 +0200 Subject: [PATCH 57/78] Finish create/edit components --- .../competency-form.component.ts | 18 ++----- ...petency-recommendation-detail.component.ts | 8 +-- .../create-prerequisite.component.ts | 20 ++++--- .../edit-prerequisite.component.ts | 23 ++++---- .../prerequisite-form.component.html | 53 ++++++------------- .../prerequisite-form.component.ts | 49 +++-------------- .../competencies/prerequisite.service.ts | 6 +-- .../webapp/app/entities/competency.model.ts | 2 +- src/main/webapp/i18n/de/competency.json | 19 ++++++- src/main/webapp/i18n/en/competency.json | 19 ++++++- .../competency-form.component.spec.ts | 29 +++++++++- .../competencies/prerequisite.service.spec.ts | 6 +-- 12 files changed, 121 insertions(+), 131 deletions(-) diff --git a/src/main/webapp/app/course/competencies/competency-form/competency-form.component.ts b/src/main/webapp/app/course/competencies/competency-form/competency-form.component.ts index 4c887539db2e..a42c623bede8 100644 --- a/src/main/webapp/app/course/competencies/competency-form/competency-form.component.ts +++ b/src/main/webapp/app/course/competencies/competency-form/competency-form.component.ts @@ -8,7 +8,7 @@ import { LectureUnit } from 'app/entities/lecture-unit/lectureUnit.model'; import { TranslateService } from '@ngx-translate/core'; import { LectureUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/lectureUnit.service'; import { intersection } from 'lodash-es'; -import { CompetencyTaxonomy, CompetencyValidators, DEFAULT_MASTERY_THRESHOLD } from 'app/entities/competency.model'; +import { CompetencyTaxonomy, CourseCompetencyValidators, DEFAULT_MASTERY_THRESHOLD } from 'app/entities/competency.model'; import { faQuestionCircle, faTimes } from '@fortawesome/free-solid-svg-icons'; import dayjs from 'dayjs/esm'; @@ -23,12 +23,9 @@ export const titleUniqueValidator = (competencyService: CompetencyService, cours if (initialTitle && title === initialTitle) { return of(null); } - return competencyService.getAllForCourse(courseId).pipe( + return competencyService.getCourseCompetencyTitles(courseId).pipe( map((res) => { - let competencyTitles: string[] = []; - if (res.body) { - competencyTitles = res.body.map((competency) => competency.title!); - } + const competencyTitles = res.body!; if (title && competencyTitles.includes(title)) { return { titleUnique: { valid: false }, @@ -90,9 +87,8 @@ export class CompetencyFormComponent implements OnInit, OnChanges { @Output() onCancel: EventEmitter = new EventEmitter(); - titleUniqueValidator = titleUniqueValidator; protected readonly competencyTaxonomy = CompetencyTaxonomy; - protected readonly competencyValidators = CompetencyValidators; + protected readonly competencyValidators = CourseCompetencyValidators; @Output() formSubmitted: EventEmitter = new EventEmitter(); @@ -168,11 +164,7 @@ export class CompetencyFormComponent implements OnInit, OnChanges { initialTitle = this.formData.title; } this.form = this.fb.nonNullable.group({ - title: [ - undefined as string | undefined, - [Validators.required, Validators.maxLength(255)], - [this.titleUniqueValidator(this.competencyService, this.courseId, initialTitle)], - ], + title: [undefined as string | undefined, [Validators.required, Validators.maxLength(255)], [titleUniqueValidator(this.competencyService, this.courseId, initialTitle)]], description: [undefined as string | undefined, [Validators.maxLength(10000)]], softDueDate: [undefined], taxonomy: [undefined as CompetencyTaxonomy | undefined], diff --git a/src/main/webapp/app/course/competencies/generate-competencies/competency-recommendation-detail.component.ts b/src/main/webapp/app/course/competencies/generate-competencies/competency-recommendation-detail.component.ts index ff7781eda7cb..5e44f690d966 100644 --- a/src/main/webapp/app/course/competencies/generate-competencies/competency-recommendation-detail.component.ts +++ b/src/main/webapp/app/course/competencies/generate-competencies/competency-recommendation-detail.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { CompetencyValidators } from 'app/entities/competency.model'; +import { CourseCompetencyValidators } from 'app/entities/competency.model'; import { faChevronRight, faPencilAlt, faSave, faTrash, faWrench } from '@fortawesome/free-solid-svg-icons'; import { ButtonSize, ButtonType } from 'app/shared/components/button.component'; import { FormGroup, Validators } from '@angular/forms'; @@ -26,13 +26,13 @@ export class CompetencyRecommendationDetailComponent implements OnInit { protected readonly faPencilAlt = faPencilAlt; //Other constants for html - protected readonly competencyValidators = CompetencyValidators; + protected readonly competencyValidators = CourseCompetencyValidators; protected readonly ButtonType = ButtonType; protected readonly ButtonSize = ButtonSize; ngOnInit(): void { - this.titleControl.addValidators([Validators.required, Validators.maxLength(CompetencyValidators.TITLE_MAX)]); - this.descriptionControl.addValidators([Validators.maxLength(CompetencyValidators.DESCRIPTION_MAX)]); + this.titleControl.addValidators([Validators.required, Validators.maxLength(CourseCompetencyValidators.TITLE_MAX)]); + this.descriptionControl.addValidators([Validators.maxLength(CourseCompetencyValidators.DESCRIPTION_MAX)]); //disable all competency controls as component is not in edit mode this.form.controls.competency.disable(); //viewed checkbox is always enabled diff --git a/src/main/webapp/app/course/competencies/prerequisite-form/create-prerequisite.component.ts b/src/main/webapp/app/course/competencies/prerequisite-form/create-prerequisite.component.ts index dd80eac44d87..fe73ac99bc23 100644 --- a/src/main/webapp/app/course/competencies/prerequisite-form/create-prerequisite.component.ts +++ b/src/main/webapp/app/course/competencies/prerequisite-form/create-prerequisite.component.ts @@ -1,6 +1,6 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, inject } from '@angular/core'; import { onError } from 'app/shared/util/global.utils'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute } from '@angular/router'; import { AlertService } from 'app/core/util/alert.service'; import { finalize } from 'rxjs/operators'; import { HttpErrorResponse } from '@angular/common/http'; @@ -8,24 +8,22 @@ import { Prerequisite } from 'app/entities/prerequisite.model'; import { PrerequisiteService } from 'app/course/competencies/prerequisite.service'; import { PrerequisiteFormComponent } from 'app/course/competencies/prerequisite-form/prerequisite-form.component'; import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; @Component({ selector: 'jhi-create-prerequisite', templateUrl: './create-prerequisite.component.html', standalone: true, - styles: [], imports: [PrerequisiteFormComponent, ArtemisSharedModule], }) export class CreatePrerequisiteComponent implements OnInit { isLoading: boolean; courseId: number; - constructor( - private activatedRoute: ActivatedRoute, - private router: Router, - private alertService: AlertService, - private prerequisiteService: PrerequisiteService, - ) {} + private readonly activatedRoute: ActivatedRoute = inject(ActivatedRoute); + private readonly alertService: AlertService = inject(AlertService); + private readonly prerequisiteService: PrerequisiteService = inject(PrerequisiteService); + private readonly navigationUtilService: ArtemisNavigationUtilService = inject(ArtemisNavigationUtilService); ngOnInit(): void { this.isLoading = true; @@ -46,13 +44,13 @@ export class CreatePrerequisiteComponent implements OnInit { ) .subscribe({ next: () => { - this.router.navigate(['../../'], { relativeTo: this.activatedRoute }); + this.navigationUtilService.navigateBack(['course-management', this.courseId, 'competency-management']); }, error: (res: HttpErrorResponse) => onError(this.alertService, res), }); } cancel() { - this.router.navigate(['../../'], { relativeTo: this.activatedRoute }); + this.navigationUtilService.navigateBack(['course-management', this.courseId, 'competency-management']); } } diff --git a/src/main/webapp/app/course/competencies/prerequisite-form/edit-prerequisite.component.ts b/src/main/webapp/app/course/competencies/prerequisite-form/edit-prerequisite.component.ts index 02f08ba984fd..4177fee7c385 100644 --- a/src/main/webapp/app/course/competencies/prerequisite-form/edit-prerequisite.component.ts +++ b/src/main/webapp/app/course/competencies/prerequisite-form/edit-prerequisite.component.ts @@ -1,6 +1,5 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, inject } from '@angular/core'; import { onError } from 'app/shared/util/global.utils'; -import { ActivatedRoute, Router } from '@angular/router'; import { AlertService } from 'app/core/util/alert.service'; import { finalize, switchMap } from 'rxjs/operators'; import { HttpErrorResponse } from '@angular/common/http'; @@ -8,12 +7,13 @@ import { Prerequisite } from 'app/entities/prerequisite.model'; import { PrerequisiteService } from 'app/course/competencies/prerequisite.service'; import { PrerequisiteFormComponent } from 'app/course/competencies/prerequisite-form/prerequisite-form.component'; import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; +import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'jhi-edit-prerequisite', templateUrl: './edit-prerequisite.component.html', standalone: true, - styles: [], imports: [PrerequisiteFormComponent, ArtemisSharedModule], }) export class EditPrerequisiteComponent implements OnInit { @@ -21,12 +21,10 @@ export class EditPrerequisiteComponent implements OnInit { courseId: number; existingPrerequisite: Prerequisite; - constructor( - private activatedRoute: ActivatedRoute, - private router: Router, - private alertService: AlertService, - private prerequisiteService: PrerequisiteService, - ) {} + private readonly activatedRoute: ActivatedRoute = inject(ActivatedRoute); + private readonly alertService: AlertService = inject(AlertService); + private readonly prerequisiteService: PrerequisiteService = inject(PrerequisiteService); + private readonly navigationUtilService: ArtemisNavigationUtilService = inject(ArtemisNavigationUtilService); ngOnInit(): void { this.isLoading = true; @@ -35,13 +33,14 @@ export class EditPrerequisiteComponent implements OnInit { switchMap((params) => { console.log(params); const prerequisiteId = Number(params['prerequisiteId']); - this.courseId = params['courseId']; + this.courseId = Number(params['courseId']); return this.prerequisiteService.getPrerequisite(prerequisiteId, this.courseId); }), ) .subscribe({ next: (prerequisite) => { this.existingPrerequisite = prerequisite; + this.isLoading = false; }, error: (res: HttpErrorResponse) => onError(this.alertService, res), }); @@ -58,13 +57,13 @@ export class EditPrerequisiteComponent implements OnInit { ) .subscribe({ next: () => { - this.router.navigate(['../../'], { relativeTo: this.activatedRoute }); + this.navigationUtilService.navigateBack(['course-management', this.courseId, 'competency-management']); }, error: (res: HttpErrorResponse) => onError(this.alertService, res), }); } cancel() { - this.router.navigate(['../../'], { relativeTo: this.activatedRoute }); + this.navigationUtilService.navigateBack(['course-management', this.courseId, 'competency-management']); } } diff --git a/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.html b/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.html index 1d08dc5dca34..f50401f1c8f1 100644 --- a/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.html +++ b/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.html @@ -2,33 +2,24 @@ @if (form) {
- - + + @if (form.controls.title.invalid && (form.controls.title.dirty || form.controls.title.touched)) {
@if (form.controls.title.errors?.required) { - + } @if (form.controls.title.errors?.maxlength) { - + } @if (form.controls.title.errors?.titleUnique) { - + }
}
- + @if (form.controls.description.invalid && form.controls.description.dirty) {
- +
}
-
- - - -
-
- - - -
+ +
- - + +
} diff --git a/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.ts b/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.ts index d0e7d2904395..bd2ee0518c9e 100644 --- a/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.ts +++ b/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.ts @@ -1,10 +1,9 @@ import { Component, EventEmitter, Input, OnInit, Output, inject } from '@angular/core'; import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { CompetencyService } from 'app/course/competencies/competency.service'; -import { merge, of } from 'rxjs'; -import { catchError, delay, map, switchMap } from 'rxjs/operators'; +import { merge } from 'rxjs'; import { TranslateService } from '@ngx-translate/core'; -import { CompetencyTaxonomy, CompetencyValidators, DEFAULT_MASTERY_THRESHOLD } from 'app/entities/competency.model'; +import { CompetencyTaxonomy, CourseCompetencyValidators, DEFAULT_MASTERY_THRESHOLD } from 'app/entities/competency.model'; import { faBan, faQuestionCircle, faSave, faTimes } from '@fortawesome/free-solid-svg-icons'; import dayjs from 'dayjs/esm'; import { ButtonSize, ButtonType } from 'app/shared/components/button.component'; @@ -15,38 +14,7 @@ import { ArtemisSharedComponentModule } from 'app/shared/components/shared-compo import { FormDateTimePickerModule } from 'app/shared/date-time-picker/date-time-picker.module'; import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; - -/** - * Async Validator to make sure that a competency title is unique within a course - */ -export const titleUniqueValidator = (competencyService: CompetencyService, courseId: number, initialTitle?: string) => { - return (competencyTitleControl: FormControl) => { - return of(competencyTitleControl.value).pipe( - delay(250), - switchMap((title) => { - if (initialTitle && title === initialTitle) { - return of(null); - } - return competencyService.getCourseCompetencyTitles(courseId).pipe( - map((res) => { - let competencyTitles: string[] = []; - if (res.body) { - competencyTitles = res.body; - } - if (title && competencyTitles.includes(title)) { - return { - titleUnique: { valid: false }, - }; - } else { - return null; - } - }), - catchError(() => of(null)), - ); - }), - ); - }; -}; +import { titleUniqueValidator } from '../competency-form/competency-form.component'; @Component({ selector: 'jhi-prerequisite-form', @@ -82,7 +50,6 @@ export class PrerequisiteFormComponent implements OnInit { optional: FormControl; }>; protected suggestedTaxonomies: string[] = []; - private titleUniqueValidator = titleUniqueValidator; // Icons protected readonly faTimes = faTimes; @@ -91,7 +58,7 @@ export class PrerequisiteFormComponent implements OnInit { protected readonly faSave = faSave; // Constants protected readonly competencyTaxonomy = CompetencyTaxonomy; - protected readonly competencyValidators = CompetencyValidators; + protected readonly competencyValidators = CourseCompetencyValidators; protected readonly ButtonSize = ButtonSize; protected readonly ButtonType = ButtonType; // Services @@ -103,15 +70,15 @@ export class PrerequisiteFormComponent implements OnInit { this.form = this.formBuilder.nonNullable.group({ title: [ this.prerequisite?.title, - [Validators.required, Validators.maxLength(CompetencyValidators.TITLE_MAX)], - [this.titleUniqueValidator(this.competencyService, this.courseId, this.prerequisite?.title)], + [Validators.required, Validators.maxLength(CourseCompetencyValidators.TITLE_MAX)], + [titleUniqueValidator(this.competencyService, this.courseId, this.prerequisite?.title)], ], - description: [this.prerequisite?.description, [Validators.maxLength(CompetencyValidators.DESCRIPTION_MAX)]], + description: [this.prerequisite?.description, [Validators.maxLength(CourseCompetencyValidators.DESCRIPTION_MAX)]], taxonomy: [this.prerequisite?.taxonomy], softDueDate: [this.prerequisite?.softDueDate], masteryThreshold: [ this.prerequisite?.masteryThreshold ?? DEFAULT_MASTERY_THRESHOLD, - [Validators.min(CompetencyValidators.MASTERY_THRESHOLD_MIN), Validators.max(CompetencyValidators.MASTERY_THRESHOLD_MAX)], + [Validators.min(CourseCompetencyValidators.MASTERY_THRESHOLD_MIN), Validators.max(CourseCompetencyValidators.MASTERY_THRESHOLD_MAX)], ], optional: [this.prerequisite?.optional ?? false], }); diff --git a/src/main/webapp/app/course/competencies/prerequisite.service.ts b/src/main/webapp/app/course/competencies/prerequisite.service.ts index 070b170e83cf..136d8f50470c 100644 --- a/src/main/webapp/app/course/competencies/prerequisite.service.ts +++ b/src/main/webapp/app/course/competencies/prerequisite.service.ts @@ -30,14 +30,14 @@ export class PrerequisiteService { const prerequisiteDTO = this.convertToRequestDTO(prerequisite); return this.httpClient .post(`${this.resourceURL}/courses/${courseId}/competencies/prerequisites`, prerequisiteDTO, { observe: 'response' }) - .pipe(map((resp) => (resp.body ? PrerequisiteService.convertResponseDTOToPrerequisite(resp.body) : undefined))); + .pipe(map((resp) => PrerequisiteService.convertResponseDTOToPrerequisite(resp.body!))); } updatePrerequisite(prerequisite: Prerequisite, prerequisiteId: number, courseId: number): Observable { const prerequisiteDTO = this.convertToRequestDTO(prerequisite); return this.httpClient - .post(`${this.resourceURL}/courses/${courseId}/competencies/prerequisites/${prerequisiteId}`, prerequisiteDTO, { observe: 'response' }) - .pipe(map((resp) => (resp.body ? PrerequisiteService.convertResponseDTOToPrerequisite(resp.body) : undefined))); + .put(`${this.resourceURL}/courses/${courseId}/competencies/prerequisites/${prerequisiteId}`, prerequisiteDTO, { observe: 'response' }) + .pipe(map((resp) => PrerequisiteService.convertResponseDTOToPrerequisite(resp.body!))); } deletePrerequisite(prerequisiteId: number, courseId: number) { diff --git a/src/main/webapp/app/entities/competency.model.ts b/src/main/webapp/app/entities/competency.model.ts index 48cdd3d557f5..8e8950b9b7e1 100644 --- a/src/main/webapp/app/entities/competency.model.ts +++ b/src/main/webapp/app/entities/competency.model.ts @@ -32,7 +32,7 @@ export enum CompetencyRelationError { EXISTING = 'EXISTING', } -export enum CompetencyValidators { +export enum CourseCompetencyValidators { TITLE_MAX = 255, DESCRIPTION_MAX = 10000, MASTERY_THRESHOLD_MIN = 0, diff --git a/src/main/webapp/i18n/de/competency.json b/src/main/webapp/i18n/de/competency.json index 1317876a680f..bc86829553d1 100644 --- a/src/main/webapp/i18n/de/competency.json +++ b/src/main/webapp/i18n/de/competency.json @@ -118,7 +118,7 @@ "titlePlaceholder": "Gib der Kompetenz einen Titel", "titleRequiredValidationError": "Eine Kompetenz braucht einen Titel", "titleMaxLengthValidationError": "Der Titel einer Kompetenz ist auf maximal {{max}} Symbole beschränkt", - "titleUniqueValidationError": "Es gibt bereits eine Kompetenz mit diesem Titel in dem Kurs", + "titleUniqueValidationError": "Es gibt bereits eine Kompetenz/Voraussetzung mit diesem Titel in dem Kurs", "descriptionMaxLengthValidationError": "Die Beschreibung der Kompetenz ist auf maximal {{max}} Symbole beschränkt", "softDueDate": "Empfohlenes Fertigstellungsdatum", "softDueDateHint": "Empfehle einen Zeitpunkt zu dem die Studierenden diese Kompetenz gemeistert haben sollten. Dies ist keine harte Frist.", @@ -189,6 +189,12 @@ } } }, + "courseCompetency": { + "title": "Titel", + "description": "Beschreibung", + "taxonomy": "Taxonomie", + "softDueDate": "Empfohlen bis" + }, "prerequisite": { "title": "Voraussetzungen", "empty": "Es existieren keine Voraussetzungen für diesen Kurs", @@ -208,7 +214,16 @@ "selectedTableEmpty": "Keine Kompetenzen zum Import als Voraussetzungen ausgewählt" }, "create": { - "title": "Erstelle eine neue Voraussetzung" + "title": "Erstelle eine neue Voraussetzung", + "titlePlaceholder": "Gib der Voraussetzung einen Titel", + "softDueDateTooltip": "Empfehle einen Zeitpunkt zu dem die Studierenden diese Voraussetzung gemeistert haben sollten. Dies ist keine harte Frist.", + "suggestedTaxonomy": "Vorschlag", + "error": { + "titleRequired": "Eine Voraussetzung braucht einen Titel", + "titleMaxLength": "Der Titel einer Voraussetzung ist auf maximal {{max}} Symbole beschränkt", + "descriptionMaxLength": "Die Beschreibung der Voraussetzung ist auf maximal {{max}} Symbole beschränkt", + "titleUnique": "Es gibt bereits eine Kompetenz/Voraussetzung mit diesem Titel in dem Kurs" + } }, "edit": { "title": "Bearbeite eine Voraussetzung" diff --git a/src/main/webapp/i18n/en/competency.json b/src/main/webapp/i18n/en/competency.json index e01a0b46ccc8..986b46021045 100644 --- a/src/main/webapp/i18n/en/competency.json +++ b/src/main/webapp/i18n/en/competency.json @@ -118,7 +118,7 @@ "titlePlaceholder": "Give the competency a title", "titleRequiredValidationError": "A competency needs to have a title", "titleMaxLengthValidationError": "A competency title is limited to {{max}} characters", - "titleUniqueValidationError": "There already exists a competency with this title in the course", + "titleUniqueValidationError": "There already exists a competency/prerequisite with this title in the course", "descriptionMaxLengthValidationError": "A competency description is limited to {{max}} characters", "softDueDate": "Recommended date of completion", "softDueDateHint": "Guide students by selecting when you expect them to have mastered this competency. This is not a hard deadline.", @@ -189,6 +189,12 @@ } } }, + "courseCompetency": { + "title": "Title", + "description": "Description", + "taxonomy": "Taxonomy", + "softDueDate": "Recommended until" + }, "prerequisite": { "title": "Prerequisites", "empty": "No prerequisites exist for this course", @@ -208,7 +214,16 @@ "selectedTableEmpty": "No competencies selected to import as prerequisites" }, "create": { - "title": "Create a new Prerequisite" + "title": "Create a new Prerequisite", + "titlePlaceholder": "Give the prerequisite a title", + "softDueDateTooltip": "Guide students by selecting when you expect them to have mastered this prerequisite. This is not a hard deadline.", + "suggestedTaxonomy": "Suggested", + "error": { + "titleRequired": "A prerequisite needs to have a title", + "titleMaxLength": "The title of a prerequisite is limited to {{max}} characters", + "descriptionMaxLength": "The description of a prerequisite is limited to {{max}} characters", + "titleUnique": "There already exists a competency/prerequisite with this title in the course" + } }, "edit": { "title": "Edit a Prerequisite" diff --git a/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts b/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts index f60e3fa7cde4..0662484f00ce 100644 --- a/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts @@ -1,5 +1,5 @@ import { HttpResponse } from '@angular/common/http'; -import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { ComponentFixture, TestBed, fakeAsync, flush, tick } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { CompetencyFormComponent, CompetencyFormData } from 'app/course/competencies/competency-form/competency-form.component'; @@ -156,6 +156,33 @@ describe('CompetencyFormComponent', () => { expect(competencyFormComponent.suggestedTaxonomies).toEqual(['artemisApp.competency.taxonomies.REMEMBER', 'artemisApp.competency.taxonomies.UNDERSTAND']); }); + it('validator should verify title is unique', fakeAsync(() => { + const competencyService = TestBed.inject(CompetencyService); + const existingTitles = ['nameExisting']; + jest.spyOn(competencyService, 'getCourseCompetencyTitles').mockReturnValue(of(new HttpResponse({ body: existingTitles, status: 200 }))); + competencyFormComponent.isEditMode = true; + competencyFormComponent.formData.title = 'initialName'; + + competencyFormComponentFixture.detectChanges(); + + const titleControl = competencyFormComponent.titleControl!; + tick(250); + expect(titleControl.errors?.titleUnique).toBeUndefined(); + + titleControl.setValue('anotherName'); + tick(250); + expect(titleControl.errors?.titleUnique).toBeUndefined(); + + titleControl.setValue(''); + tick(250); + expect(titleControl.errors?.titleUnique).toBeUndefined(); + + titleControl.setValue('nameExisting'); + tick(250); + expect(titleControl.errors?.titleUnique).toBeDefined(); + flush(); + })); + function createTranslateSpy() { return jest.spyOn(translateService, 'instant').mockImplementation((key) => { switch (key) { diff --git a/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts b/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts index 528e7e40bb97..c50ce64b6799 100644 --- a/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts +++ b/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts @@ -81,14 +81,14 @@ describe('PrerequisiteService', () => { const expectedPrerequisite: Prerequisite = { id: 1, title: 'newTitle', description: 'newDescription' }; const returnedFromService: Prerequisite = { ...expectedPrerequisite }; - prerequisiteService.createPrerequisite({ title: 'newTitle', description: 'newDescription' }, 1, 1).subscribe((resp) => (actualPrerequisite = resp)); - const req = httpTestingController.expectOne({ method: 'POST' }); + prerequisiteService.createPrerequisite({ title: 'newTitle', description: 'newDescription' }, 1).subscribe((resp) => (actualPrerequisite = resp)); + const req = httpTestingController.expectOne({ method: 'PUT' }); req.flush(returnedFromService); tick(); expect(actualPrerequisite).toEqual(expectedPrerequisite); })); - + it('should convert response dtos to to prerequisite', () => { const expectedPrerequisite1: Prerequisite = { id: 1, title: 'title1', linkedCourseCompetency: { id: 1, course: { id: 1, title: '', semester: 'SS01' } } }; const prerequisiteDTO1: PrerequisiteResponseDTO = { From bc0c1c8a1d6c4b0afc6d0d903e79fee5369a246d Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Sun, 9 Jun 2024 12:46:49 +0200 Subject: [PATCH 58/78] Adjust competency management --- .../competency-management.component.html | 16 +++++++++++----- src/main/webapp/i18n/de/competency.json | 2 ++ src/main/webapp/i18n/en/competency.json | 2 ++ 3 files changed, 15 insertions(+), 5 deletions(-) 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 00ab1fd9beff..48118bf620ba 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 @@ -147,7 +147,7 @@

- +
@@ -160,16 +160,19 @@

+ @@ -186,6 +189,9 @@

{{ 'artemisApp.competency.taxonomies.' + (prerequisite.taxonomy ?? 'none') | artemisTranslate }} +
- + +
+ + + + + +
- + - + - + - + + +
+ {{ prerequisite.softDueDate | artemisDate }} + @if (prerequisite.linkedCourseCompetency?.course?.id) {
diff --git a/src/main/webapp/i18n/de/competency.json b/src/main/webapp/i18n/de/competency.json index bc86829553d1..4414d163d34c 100644 --- a/src/main/webapp/i18n/de/competency.json +++ b/src/main/webapp/i18n/de/competency.json @@ -191,6 +191,7 @@ }, "courseCompetency": { "title": "Titel", + "course": "Kurs", "description": "Beschreibung", "taxonomy": "Taxonomie", "softDueDate": "Empfohlen bis" @@ -199,6 +200,7 @@ "title": "Voraussetzungen", "empty": "Es existieren keine Voraussetzungen für diesen Kurs", "manage": { + "createButton": "Voraussetzung erstellen", "importButton": "Voraussetzungen importieren", "importFromCoursesButton": "Aus anderen Kursen importieren", "deleted": "Voraussetzung gelöscht", diff --git a/src/main/webapp/i18n/en/competency.json b/src/main/webapp/i18n/en/competency.json index 986b46021045..ee1a325be11e 100644 --- a/src/main/webapp/i18n/en/competency.json +++ b/src/main/webapp/i18n/en/competency.json @@ -191,6 +191,7 @@ }, "courseCompetency": { "title": "Title", + "course": "Kurs", "description": "Description", "taxonomy": "Taxonomy", "softDueDate": "Recommended until" @@ -199,6 +200,7 @@ "title": "Prerequisites", "empty": "No prerequisites exist for this course", "manage": { + "createButton": "Create prerequisite", "importButton": "Import prerequisites", "importFromCoursesButton": "Import from other courses", "deleted": "Prerequisite deleted", From 8024cf3ed6216eb05319a4911b3c306809e206aa Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Sun, 9 Jun 2024 12:47:26 +0200 Subject: [PATCH 59/78] remove TODO --- .../prerequisite-form/prerequisite-form.component.html | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.html b/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.html index f50401f1c8f1..74b00708b6ec 100644 --- a/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.html +++ b/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.html @@ -49,7 +49,6 @@
-
From c99c65ba05f507fa644da88ad3fa553e21d815f7 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Sun, 9 Jun 2024 13:05:17 +0200 Subject: [PATCH 60/78] Add server test --- .../repository/PrerequisiteRepository.java | 2 +- .../artemis/web/rest/CompetencyResource.java | 5 ++-- .../rest/competency/PrerequisiteResource.java | 2 +- .../lecture/CompetencyIntegrationTest.java | 28 +++++++++++++++++++ .../lecture/PrerequisiteIntegrationTest.java | 4 +-- 5 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/PrerequisiteRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/PrerequisiteRepository.java index bc316a2a5d58..3ae3495e3110 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/PrerequisiteRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/PrerequisiteRepository.java @@ -14,7 +14,7 @@ */ public interface PrerequisiteRepository extends JpaRepository { - List findByCourseIdOrderById(long courseId); + List findAllByCourseIdOrderById(long courseId); Optional findByIdAndCourseId(long prerequisiteId, long courseId); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java index 850fba2e268e..ff9c3aa7b2a9 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java @@ -107,14 +107,15 @@ public class CompetencyResource { private final CompetencyRelationService competencyRelationService; private final CourseCompetencyRepository courseCompetencyRepository; - + private final CompetencyJolService competencyJolService; 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) { + Optional irisCompetencyGenerationSessionService, CourseCompetencyRepository courseCompetencyRepository, + CompetencyJolService competencyJolService) { this.courseRepository = courseRepository; this.competencyRelationRepository = competencyRelationRepository; this.authorizationCheckService = authorizationCheckService; 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 d43526c93a34..3843a34b6a24 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 @@ -75,7 +75,7 @@ public ResponseEntity> getPrerequisites(@PathVaria authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, null); } - var prerequisites = prerequisiteRepository.findByCourseIdOrderById(courseId); + var prerequisites = prerequisiteRepository.findAllByCourseIdOrderById(courseId); return ResponseEntity.ok(prerequisites.stream().map(PrerequisiteResponseDTO::of).toList()); } 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 9ccf1f195ca0..3ffa3549a938 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 @@ -9,6 +9,7 @@ import java.util.List; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -22,6 +23,7 @@ import de.tum.in.www1.artemis.AbstractSpringIntegrationLocalCILocalVCTest; 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; @@ -37,6 +39,7 @@ import de.tum.in.www1.artemis.domain.competency.CompetencyProgress; import de.tum.in.www1.artemis.domain.competency.CompetencyRelation; import de.tum.in.www1.artemis.domain.competency.CompetencyTaxonomy; +import de.tum.in.www1.artemis.domain.competency.CourseCompetency; import de.tum.in.www1.artemis.domain.competency.RelationType; import de.tum.in.www1.artemis.domain.enumeration.ExerciseMode; import de.tum.in.www1.artemis.domain.enumeration.IncludedInOverallScore; @@ -54,6 +57,7 @@ import de.tum.in.www1.artemis.repository.ExerciseUnitRepository; import de.tum.in.www1.artemis.repository.LectureRepository; import de.tum.in.www1.artemis.repository.LectureUnitRepository; +import de.tum.in.www1.artemis.repository.PrerequisiteRepository; import de.tum.in.www1.artemis.repository.ResultRepository; import de.tum.in.www1.artemis.repository.SubmissionRepository; import de.tum.in.www1.artemis.repository.TextUnitRepository; @@ -103,6 +107,9 @@ class CompetencyIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCT @Autowired private CompetencyRelationRepository competencyRelationRepository; + @Autowired + private PrerequisiteRepository prerequisiteRepository; + @Autowired private LectureUnitRepository lectureUnitRepository; @@ -121,6 +128,9 @@ class CompetencyIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCT @Autowired private CompetencyUtilService competencyUtilService; + @Autowired + private PrerequisiteUtilService prerequisiteUtilService; + @Autowired private PageableSearchUtilService pageableSearchUtilService; @@ -981,4 +991,22 @@ void shouldGetCompetenciesAsAdmin() throws Exception { assertThat(result.getResultsOnPage()).hasSize(1); } } + + @Nested + class GetCourseCompetencyTitles { + + @Test + @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(); + prerequisiteUtilService.createPrerequisites(course, 5); + var prerequisiteTitles = prerequisiteRepository.findAllByCourseIdOrderById(course.getId()).stream().map(CourseCompetency::getTitle).toList(); + var expectedTitles = Stream.concat(competencyTitles.stream(), prerequisiteTitles.stream()).toList(); + + final var actualTitles = request.getList("/api/courses/" + course.getId() + "/competencies/titles", HttpStatus.OK, String.class); + + assertThat(actualTitles).containsAll(expectedTitles); + } + } } 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 6de28d58d792..b3db296bf3be 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 @@ -74,7 +74,7 @@ private static String url(long courseId) { @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void shouldReturnPrerequisites() throws Exception { prerequisiteUtilService.createPrerequisites(course, 5); - var prerequisites = prerequisiteRepository.findByCourseIdOrderById(course.getId()); + var prerequisites = prerequisiteRepository.findAllByCourseIdOrderById(course.getId()); var expectedPrerequisites = prerequisites.stream().map(PrerequisiteResponseDTO::of).toList(); List actualPrerequisites = request.getList(url(course.getId()), HttpStatus.OK, PrerequisiteResponseDTO.class); @@ -89,7 +89,7 @@ void shouldReturnPrerequisitesForStudentNotInCourseIfEnrollmentIsEnabled() throw course.setEnrollmentEnabled(true); courseRepository.save(course); prerequisiteUtilService.createPrerequisites(course, 5); - var prerequisites = prerequisiteRepository.findByCourseIdOrderById(course.getId()); + var prerequisites = prerequisiteRepository.findAllByCourseIdOrderById(course.getId()); var expectedPrerequisites = prerequisites.stream().map(PrerequisiteResponseDTO::of).toList(); List actualPrerequisites = request.getList(url(course.getId()), HttpStatus.OK, PrerequisiteResponseDTO.class); From 56bcd938f3de58391fd291f68f40d521082ad080 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Sun, 9 Jun 2024 13:48:15 +0200 Subject: [PATCH 61/78] Add client tests --- .../competencies/competency.service.spec.ts | 19 ++++++++++++++++++- .../competencies/prerequisite.service.spec.ts | 2 +- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/test/javascript/spec/component/competencies/competency.service.spec.ts b/src/test/javascript/spec/component/competencies/competency.service.spec.ts index 753ba22fb65b..7d9c20927e53 100644 --- a/src/test/javascript/spec/component/competencies/competency.service.spec.ts +++ b/src/test/javascript/spec/component/competencies/competency.service.spec.ts @@ -340,7 +340,24 @@ describe('CompetencyService', () => { expect(resultGetForImport).toEqual(expected); })); - it('convert response from server', () => { + it('should get courseCompetency titles', fakeAsync(() => { + let result: HttpResponse = new HttpResponse(); + const returnedFromService = ['title1', 'title2']; + const expected = [...returnedFromService]; + + competencyService + .getCourseCompetencyTitles(1) + .pipe(take(1)) + .subscribe((resp) => (result = resp)); + + const req = httpTestingController.expectOne({ method: 'GET' }); + req.flush(returnedFromService); + tick(); + + expect(result.body).toEqual(expected); + })); + + it('should convert response from server', () => { const lectureUnitService = TestBed.inject(LectureUnitService); const accountService = TestBed.inject(AccountService); diff --git a/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts b/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts index c50ce64b6799..91bbbf14d70b 100644 --- a/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts +++ b/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts @@ -81,7 +81,7 @@ describe('PrerequisiteService', () => { const expectedPrerequisite: Prerequisite = { id: 1, title: 'newTitle', description: 'newDescription' }; const returnedFromService: Prerequisite = { ...expectedPrerequisite }; - prerequisiteService.createPrerequisite({ title: 'newTitle', description: 'newDescription' }, 1).subscribe((resp) => (actualPrerequisite = resp)); + prerequisiteService.updatePrerequisite({ title: 'newTitle', description: 'newDescription' }, 1, 1).subscribe((resp) => (actualPrerequisite = resp)); const req = httpTestingController.expectOne({ method: 'PUT' }); req.flush(returnedFromService); tick(); From 0ec391c0ebbd42f6c8a6d2626bf1fd6da4f19c98 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Sun, 9 Jun 2024 14:38:31 +0200 Subject: [PATCH 62/78] Add more client tests --- .../edit-prerequisite.component.ts | 1 - .../create-prerequisite.component.spec.ts | 112 ++++++++++++++++ .../edit-prerequisite.component.spec.ts | 122 ++++++++++++++++++ .../prerequisite-form-stub.component.ts | 19 +++ 4 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 src/test/javascript/spec/component/competencies/create-prerequisite.component.spec.ts create mode 100644 src/test/javascript/spec/component/competencies/edit-prerequisite.component.spec.ts create mode 100644 src/test/javascript/spec/component/competencies/prerequisite-form-stub.component.ts diff --git a/src/main/webapp/app/course/competencies/prerequisite-form/edit-prerequisite.component.ts b/src/main/webapp/app/course/competencies/prerequisite-form/edit-prerequisite.component.ts index 4177fee7c385..6d6b4caa7532 100644 --- a/src/main/webapp/app/course/competencies/prerequisite-form/edit-prerequisite.component.ts +++ b/src/main/webapp/app/course/competencies/prerequisite-form/edit-prerequisite.component.ts @@ -31,7 +31,6 @@ export class EditPrerequisiteComponent implements OnInit { this.activatedRoute.params .pipe( switchMap((params) => { - console.log(params); const prerequisiteId = Number(params['prerequisiteId']); this.courseId = Number(params['courseId']); return this.prerequisiteService.getPrerequisite(prerequisiteId, this.courseId); diff --git a/src/test/javascript/spec/component/competencies/create-prerequisite.component.spec.ts b/src/test/javascript/spec/component/competencies/create-prerequisite.component.spec.ts new file mode 100644 index 000000000000..d74cce6b22fc --- /dev/null +++ b/src/test/javascript/spec/component/competencies/create-prerequisite.component.spec.ts @@ -0,0 +1,112 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockProvider } from 'ng-mocks'; +import { AlertService } from 'app/core/util/alert.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { of, throwError } from 'rxjs'; +import { MockRouter } from '../../helpers/mocks/mock-router'; +import { CreatePrerequisiteComponent } from 'app/course/competencies/prerequisite-form/create-prerequisite.component'; +import { PrerequisiteService } from 'app/course/competencies/prerequisite.service'; +import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; +import { MockTranslateService } from '../../helpers/mocks/service/mock-translate.service'; +import { TranslateService } from '@ngx-translate/core'; +import { Prerequisite } from 'app/entities/prerequisite.model'; +import { CompetencyTaxonomy } from 'app/entities/competency.model'; +import dayjs from 'dayjs'; +import { Dayjs } from 'dayjs/esm'; +import { HttpErrorResponse, provideHttpClient } from '@angular/common/http'; +import { PrerequisiteFormComponent } from 'app/course/competencies/prerequisite-form/prerequisite-form.component'; +import { PrerequisiteFormStubComponent } from './prerequisite-form-stub.component'; +import { By } from '@angular/platform-browser'; + +describe('CreatePrerequisiteComponent', () => { + let componentFixture: ComponentFixture; + let component: CreatePrerequisiteComponent; + let prerequisiteService: PrerequisiteService; + const prerequisite: Prerequisite = { + title: 'Title1', + description: 'Description1', + taxonomy: CompetencyTaxonomy.APPLY, + masteryThreshold: 50, + optional: true, + softDueDate: dayjs('2022-02-20') as Dayjs, + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [CreatePrerequisiteComponent], + providers: [ + provideHttpClient(), + MockProvider(PrerequisiteService), + MockProvider(ArtemisNavigationUtilService), + MockProvider(AlertService), + { + provide: TranslateService, + useClass: MockTranslateService, + }, + { provide: Router, useClass: MockRouter }, + { + provide: ActivatedRoute, + useValue: { + params: of({ + prerequisiteId: 1, + courseId: 1, + }), + }, + }, + ], + }) + .overrideComponent(CreatePrerequisiteComponent, { + remove: { + imports: [PrerequisiteFormComponent], + }, + add: { + imports: [PrerequisiteFormStubComponent], + }, + }) + .compileComponents() + .then(() => { + componentFixture = TestBed.createComponent(CreatePrerequisiteComponent); + component = componentFixture.componentInstance; + prerequisiteService = TestBed.inject(PrerequisiteService); + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should navigate back after creating prerequisite', () => { + const createSpy = jest.spyOn(prerequisiteService, 'createPrerequisite').mockReturnValue(of(prerequisite)); + const navigationUtilService = TestBed.inject(ArtemisNavigationUtilService); + const navigateSpy = jest.spyOn(navigationUtilService, 'navigateBack'); + + componentFixture.detectChanges(); + const prerequisiteForm: PrerequisiteFormStubComponent = componentFixture.debugElement.query(By.directive(PrerequisiteFormStubComponent)).componentInstance; + prerequisiteForm.onSubmit.emit(prerequisite); + + expect(createSpy).toHaveBeenCalledWith(prerequisite, component.courseId); + expect(navigateSpy).toHaveBeenCalled(); + }); + + it('should navigate on cancel', () => { + const navigationUtilService = TestBed.inject(ArtemisNavigationUtilService); + const navigateSpy = jest.spyOn(navigationUtilService, 'navigateBack'); + + componentFixture.detectChanges(); + const prerequisiteForm: PrerequisiteFormStubComponent = componentFixture.debugElement.query(By.directive(PrerequisiteFormStubComponent)).componentInstance; + prerequisiteForm.onCancel.emit(); + + expect(navigateSpy).toHaveBeenCalled(); + }); + + it('should alert on error', () => { + const alertService = TestBed.inject(AlertService); + const errorSpy = jest.spyOn(alertService, 'error'); + jest.spyOn(prerequisiteService, 'createPrerequisite').mockReturnValue(throwError(() => new HttpErrorResponse({ status: 400 }))); + + componentFixture.detectChanges(); + component.createPrerequisite(prerequisite); + + expect(errorSpy).toHaveBeenCalled(); + }); +}); diff --git a/src/test/javascript/spec/component/competencies/edit-prerequisite.component.spec.ts b/src/test/javascript/spec/component/competencies/edit-prerequisite.component.spec.ts new file mode 100644 index 000000000000..f8dcd0e7dbf5 --- /dev/null +++ b/src/test/javascript/spec/component/competencies/edit-prerequisite.component.spec.ts @@ -0,0 +1,122 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockProvider } from 'ng-mocks'; +import { AlertService } from 'app/core/util/alert.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { of, throwError } from 'rxjs'; +import { MockRouter } from '../../helpers/mocks/mock-router'; +import { EditPrerequisiteComponent } from 'app/course/competencies/prerequisite-form/edit-prerequisite.component'; +import { PrerequisiteService } from 'app/course/competencies/prerequisite.service'; +import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; +import { MockTranslateService } from '../../helpers/mocks/service/mock-translate.service'; +import { TranslateService } from '@ngx-translate/core'; +import { Prerequisite } from 'app/entities/prerequisite.model'; +import { CompetencyTaxonomy } from 'app/entities/competency.model'; +import dayjs from 'dayjs'; +import { Dayjs } from 'dayjs/esm'; +import { HttpErrorResponse, provideHttpClient } from '@angular/common/http'; +import { PrerequisiteFormComponent } from 'app/course/competencies/prerequisite-form/prerequisite-form.component'; +import { PrerequisiteFormStubComponent } from './prerequisite-form-stub.component'; +import { By } from '@angular/platform-browser'; + +describe('EditPrerequisiteComponent', () => { + let componentFixture: ComponentFixture; + let component: EditPrerequisiteComponent; + const prerequisite: Prerequisite = { + id: 1, + title: 'Title1', + description: 'Description1', + taxonomy: CompetencyTaxonomy.APPLY, + masteryThreshold: 50, + optional: true, + softDueDate: dayjs('2022-02-20') as Dayjs, + }; + let prerequisiteService: PrerequisiteService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [EditPrerequisiteComponent], + providers: [ + provideHttpClient(), + MockProvider(PrerequisiteService), + MockProvider(ArtemisNavigationUtilService), + MockProvider(AlertService), + { + provide: TranslateService, + useClass: MockTranslateService, + }, + { provide: Router, useClass: MockRouter }, + { + provide: ActivatedRoute, + useValue: { + params: of({ + prerequisiteId: 1, + courseId: 1, + }), + }, + }, + ], + }) + .overrideComponent(EditPrerequisiteComponent, { + remove: { + imports: [PrerequisiteFormComponent], + }, + add: { + imports: [PrerequisiteFormStubComponent], + }, + }) + .compileComponents() + .then(() => { + componentFixture = TestBed.createComponent(EditPrerequisiteComponent); + component = componentFixture.componentInstance; + prerequisiteService = TestBed.inject(PrerequisiteService); + jest.spyOn(prerequisiteService, 'getPrerequisite').mockReturnValue(of(prerequisite)); + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should initialize correctly', () => { + componentFixture.detectChanges(); + + expect(component.existingPrerequisite).toEqual(prerequisite); + expect(component.isLoading).toBeFalse(); + }); + + it('should navigate back after updating prerequisite', () => { + const navigationUtilService = TestBed.inject(ArtemisNavigationUtilService); + const navigateSpy = jest.spyOn(navigationUtilService, 'navigateBack'); + const updatedPrerequisite: Prerequisite = { ...prerequisite, title: 'new title', description: 'new description' }; + const updateSpy = jest.spyOn(prerequisiteService, 'updatePrerequisite').mockReturnValue(of(updatedPrerequisite)); + + componentFixture.detectChanges(); + const prerequisiteForm: PrerequisiteFormStubComponent = componentFixture.debugElement.query(By.directive(PrerequisiteFormStubComponent)).componentInstance; + prerequisiteForm.onSubmit.emit(updatedPrerequisite); + + expect(updateSpy).toHaveBeenCalledWith(updatedPrerequisite, updatedPrerequisite.id, component.courseId); + expect(navigateSpy).toHaveBeenCalled(); + }); + + it('should navigate on cancel', () => { + const navigationUtilService = TestBed.inject(ArtemisNavigationUtilService); + const navigateSpy = jest.spyOn(navigationUtilService, 'navigateBack'); + + componentFixture.detectChanges(); + const prerequisiteForm: PrerequisiteFormStubComponent = componentFixture.debugElement.query(By.directive(PrerequisiteFormStubComponent)).componentInstance; + prerequisiteForm.onCancel.emit(); + + expect(navigateSpy).toHaveBeenCalled(); + }); + + it('should alert on error', () => { + const alertService = TestBed.inject(AlertService); + const errorSpy = jest.spyOn(alertService, 'error'); + jest.spyOn(prerequisiteService, 'updatePrerequisite').mockReturnValue(throwError(() => new HttpErrorResponse({ status: 400 }))); + + componentFixture.detectChanges(); + component.updatePrerequisite(prerequisite); + + expect(errorSpy).toHaveBeenCalled(); + }); +}); diff --git a/src/test/javascript/spec/component/competencies/prerequisite-form-stub.component.ts b/src/test/javascript/spec/component/competencies/prerequisite-form-stub.component.ts new file mode 100644 index 000000000000..7aea4c2b0128 --- /dev/null +++ b/src/test/javascript/spec/component/competencies/prerequisite-form-stub.component.ts @@ -0,0 +1,19 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Prerequisite } from 'app/entities/prerequisite.model'; + +@Component({ + selector: 'jhi-prerequisite-form', + template: '', + standalone: true, +}) +export class PrerequisiteFormStubComponent { + @Input() + prerequisite?: Prerequisite; + @Input() + courseId: number; + + @Output() + onCancel: EventEmitter = new EventEmitter(); + @Output() + onSubmit: EventEmitter = new EventEmitter(); +} From da86acb16ae5233116af6383664ef11d3c5d8fc0 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Sun, 9 Jun 2024 15:30:09 +0200 Subject: [PATCH 63/78] Add more client tests --- .../prerequisite-form.component.ts | 15 +-- .../competency-form.component.spec.ts | 16 +-- .../prerequisite-form.component.spec.ts | 107 ++++++++++++++++++ 3 files changed, 113 insertions(+), 25 deletions(-) create mode 100644 src/test/javascript/spec/component/competencies/prerequisite-form.component.spec.ts diff --git a/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.ts b/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.ts index bd2ee0518c9e..3c68d06ae600 100644 --- a/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.ts +++ b/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.ts @@ -9,25 +9,16 @@ import dayjs from 'dayjs/esm'; import { ButtonSize, ButtonType } from 'app/shared/components/button.component'; import { Prerequisite } from 'app/entities/prerequisite.model'; import { ArtemisCompetenciesModule } from 'app/course/competencies/competency.module'; -import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; -import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { FormDateTimePickerModule } from 'app/shared/date-time-picker/date-time-picker.module'; import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module'; -import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { titleUniqueValidator } from '../competency-form/competency-form.component'; +import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; @Component({ selector: 'jhi-prerequisite-form', templateUrl: './prerequisite-form.component.html', - imports: [ - ArtemisCompetenciesModule, - ArtemisSharedCommonModule, - ArtemisSharedComponentModule, - FormDateTimePickerModule, - ArtemisMarkdownEditorModule, - FontAwesomeModule, - ReactiveFormsModule, - ], + imports: [ArtemisCompetenciesModule, ArtemisSharedCommonModule, ArtemisSharedComponentModule, FormDateTimePickerModule, ArtemisMarkdownEditorModule, ReactiveFormsModule], standalone: true, }) export class PrerequisiteFormComponent implements OnInit { diff --git a/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts b/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts index 0662484f00ce..7f9727ddebec 100644 --- a/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts @@ -4,7 +4,7 @@ import { ReactiveFormsModule } from '@angular/forms'; import { NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { CompetencyFormComponent, CompetencyFormData } from 'app/course/competencies/competency-form/competency-form.component'; import { CompetencyService } from 'app/course/competencies/competency.service'; -import { Competency, CompetencyTaxonomy } from 'app/entities/competency.model'; +import { CompetencyTaxonomy } from 'app/entities/competency.model'; import { TextUnit } from 'app/entities/lecture-unit/textUnit.model'; import { Lecture } from 'app/entities/lecture.model'; import { LectureUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/lectureUnit.service'; @@ -59,17 +59,7 @@ describe('CompetencyFormComponent', () => { it('should submit valid form', fakeAsync(() => { // stubbing competency service for asynchronous validator const competencyService = TestBed.inject(CompetencyService); - - const competencyOfResponse: Competency = {}; - competencyOfResponse.id = 1; - competencyOfResponse.title = 'test'; - - const response: HttpResponse = new HttpResponse({ - body: [competencyOfResponse], - status: 200, - }); - - const getAllForCourseSpy = jest.spyOn(competencyService, 'getAllForCourse').mockReturnValue(of(response)); + const getAllTitlesSpy = jest.spyOn(competencyService, 'getCourseCompetencyTitles').mockReturnValue(of(new HttpResponse({ body: ['test'], status: 200 }))); competencyFormComponentFixture.detectChanges(); @@ -92,7 +82,7 @@ describe('CompetencyFormComponent', () => { competencyFormComponentFixture.detectChanges(); tick(250); // async validator fires after 250ms and fully filled in form should now be valid! expect(competencyFormComponent.form.valid).toBeTrue(); - expect(getAllForCourseSpy).toHaveBeenCalledOnce(); + expect(getAllTitlesSpy).toHaveBeenCalledOnce(); const submitFormSpy = jest.spyOn(competencyFormComponent, 'submitForm'); const submitFormEventSpy = jest.spyOn(competencyFormComponent.formSubmitted, 'emit'); diff --git a/src/test/javascript/spec/component/competencies/prerequisite-form.component.spec.ts b/src/test/javascript/spec/component/competencies/prerequisite-form.component.spec.ts new file mode 100644 index 000000000000..9f88ae91bbb8 --- /dev/null +++ b/src/test/javascript/spec/component/competencies/prerequisite-form.component.spec.ts @@ -0,0 +1,107 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockProvider } from 'ng-mocks'; +import { Router } from '@angular/router'; +import { MockRouter } from '../../helpers/mocks/mock-router'; +import { MockTranslateService } from '../../helpers/mocks/service/mock-translate.service'; +import { TranslateService } from '@ngx-translate/core'; +import { Prerequisite } from 'app/entities/prerequisite.model'; +import { CompetencyTaxonomy } from 'app/entities/competency.model'; +import dayjs from 'dayjs'; +import { Dayjs } from 'dayjs/esm'; +import { provideHttpClient } from '@angular/common/http'; +import { PrerequisiteFormComponent } from 'app/course/competencies/prerequisite-form/prerequisite-form.component'; +import { CompetencyService } from 'app/course/competencies/competency.service'; +import { ArtemisTestModule } from '../../test.module'; + +describe('PrerequisiteFormComponent', () => { + let componentFixture: ComponentFixture; + let component: PrerequisiteFormComponent; + const prerequisite: Prerequisite = { + title: 'Title1', + description: 'Description1', + taxonomy: CompetencyTaxonomy.APPLY, + masteryThreshold: 50, + optional: true, + softDueDate: dayjs('2022-02-20') as Dayjs, + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [PrerequisiteFormComponent, ArtemisTestModule], + providers: [ + provideHttpClient(), + MockProvider(CompetencyService), + { + provide: TranslateService, + useClass: MockTranslateService, + }, + { provide: Router, useClass: MockRouter }, + ], + }) + .compileComponents() + .then(() => { + componentFixture = TestBed.createComponent(PrerequisiteFormComponent); + component = componentFixture.componentInstance; + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should update description', () => { + componentFixture.detectChanges(); + component.updateDescriptionControl('new description'); + + expect(component['form'].controls.description.getRawValue()).toBe('new description'); + }); + + it('should emit on submit', () => { + component.prerequisite = prerequisite; + componentFixture.detectChanges(); + + component['form'].controls.title.setValue('new title'); + const submitSpy = jest.spyOn(component.onSubmit, 'emit'); + + component.submit(); + + prerequisite.title = 'new title'; + expect(submitSpy).toHaveBeenCalledWith(prerequisite); + }); + + it('should emit on cancel', () => { + const cancelSpy = jest.spyOn(component.onCancel, 'emit'); + + component.cancel(); + + expect(cancelSpy).toHaveBeenCalled(); + }); + + it('should suggest taxonomy when title changes', () => { + const suggestTaxonomySpy = jest.spyOn(component, 'suggestTaxonomies'); + const translateSpy = createTranslateSpy(); + componentFixture.detectChanges(); + + const titleInput = componentFixture.nativeElement.querySelector('#title'); + titleInput.value = 'Building a tool: create a plan and implement something!'; + titleInput.dispatchEvent(new Event('input')); + + expect(suggestTaxonomySpy).toHaveBeenCalledOnce(); + expect(translateSpy).toHaveBeenCalledTimes(12); + expect(component['suggestedTaxonomies']).toEqual(['artemisApp.competency.taxonomies.REMEMBER', 'artemisApp.competency.taxonomies.UNDERSTAND']); + }); + + function createTranslateSpy() { + const translateService = TestBed.inject(TranslateService); + return jest.spyOn(translateService, 'instant').mockImplementation((key) => { + switch (key) { + case 'artemisApp.competency.keywords.REMEMBER': + return 'Something'; + case 'artemisApp.competency.keywords.UNDERSTAND': + return 'invent, build'; + default: + return key; + } + }); + } +}); From d36f06f3c34b6726610eccb6332fd435bb3cd0bc Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Sun, 9 Jun 2024 15:45:10 +0200 Subject: [PATCH 64/78] Fix server tests --- .../www1/artemis/lecture/PrerequisiteIntegrationTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 b3db296bf3be..7346fca823e0 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 @@ -117,21 +117,21 @@ private static String url(long courseId, long prerequisiteId) { void shouldReturnPrerequisite() throws Exception { var prerequisite = prerequisiteUtilService.createPrerequisite(course); - PrerequisiteResponseDTO actualPrerequisite = request.get(url(prerequisite.getId(), course.getId()), HttpStatus.OK, PrerequisiteResponseDTO.class); + PrerequisiteResponseDTO actualPrerequisite = request.get(url(course.getId(), prerequisite.getId()), HttpStatus.OK, PrerequisiteResponseDTO.class); assertThat(actualPrerequisite).isEqualTo(PrerequisiteResponseDTO.of(prerequisite)); } @Test - @WithMockUser(username = TEST_PREFIX + "student42", roles = "USER") + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void shouldReturnNotFoundIfCompetencyDoesNotExistInCourse() throws Exception { var prerequisite = prerequisiteUtilService.createPrerequisite(course); // this competency does not exist in course2 - request.get(url(prerequisite.getId(), course2.getId()), HttpStatus.NOT_FOUND, PrerequisiteResponseDTO.class); + request.get(url(course2.getId(), prerequisite.getId()), HttpStatus.NOT_FOUND, PrerequisiteResponseDTO.class); // this competency does not exist at all - request.get(url(-1000L, course.getId()), HttpStatus.NOT_FOUND, PrerequisiteResponseDTO.class); + request.get(url(course.getId(), -1000L), HttpStatus.NOT_FOUND, PrerequisiteResponseDTO.class); } } From 3b2e7eb73b8c68eb3403d246c9de5c9cc2604735 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Sun, 9 Jun 2024 23:57:28 +0200 Subject: [PATCH 65/78] Fix server test --- .../artemis/lecture/CompetencyIntegrationTest.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 3ffa3549a938..f0dda32fb1b9 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 @@ -856,8 +856,14 @@ void shouldImportCompetencies() throws Exception { assertThat(competencyDTOList).hasSize(2); // competency 2 should be the tail of one relation - assertThat(competencyDTOList.get(0).tailRelations()).isNull(); - assertThat(competencyDTOList.get(1).tailRelations()).hasSize(1); + var importedHead = competencyDTOList.getFirst(); + var importedTail = competencyDTOList.get(1); + if (!importedHead.competency().getTitle().equals(head.getTitle())) { + importedHead = importedTail; + importedTail = competencyDTOList.getFirst(); + } + assertThat(importedHead.tailRelations()).isNull(); + assertThat(importedTail.tailRelations()).hasSize(1); competencyDTOList = request.postListWithResponseBody("/api/courses/" + course.getId() + "/competencies/import-all/" + course3.getId() + "?importRelations=false", null, CompetencyWithTailRelationDTO.class, HttpStatus.CREATED); From e16ea1e6a5ff8ac3f610b14d7fda611a0846154a Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Mon, 10 Jun 2024 00:25:57 +0200 Subject: [PATCH 66/78] Fix server test --- .../artemis/lecture/CompetencyIntegrationTest.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) 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 f0dda32fb1b9..4a1c6ed8df84 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 @@ -855,15 +855,13 @@ void shouldImportCompetencies() throws Exception { CompetencyWithTailRelationDTO.class, HttpStatus.CREATED); assertThat(competencyDTOList).hasSize(2); - // competency 2 should be the tail of one relation - var importedHead = competencyDTOList.getFirst(); - var importedTail = competencyDTOList.get(1); - if (!importedHead.competency().getTitle().equals(head.getTitle())) { - importedHead = importedTail; - importedTail = competencyDTOList.getFirst(); + // assert that only one of the DTOs has the relation connected + if (competencyDTOList.getFirst().tailRelations() == null) { + assertThat(competencyDTOList.get(1).tailRelations()).hasSize(1); + } + else { + assertThat(competencyDTOList.get(1).tailRelations()).isNull(); } - assertThat(importedHead.tailRelations()).isNull(); - assertThat(importedTail.tailRelations()).hasSize(1); competencyDTOList = request.postListWithResponseBody("/api/courses/" + course.getId() + "/competencies/import-all/" + course3.getId() + "?importRelations=false", null, CompetencyWithTailRelationDTO.class, HttpStatus.CREATED); From 36ea9b24941d13ffcf800b370863c66ec62411c7 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Mon, 10 Jun 2024 00:31:04 +0200 Subject: [PATCH 67/78] Add another client test --- .../competencies/prerequisite.service.spec.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts b/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts index 91bbbf14d70b..7500f48a45e7 100644 --- a/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts +++ b/src/test/javascript/spec/component/competencies/prerequisite.service.spec.ts @@ -89,6 +89,19 @@ describe('PrerequisiteService', () => { expect(actualPrerequisite).toEqual(expectedPrerequisite); })); + it('should get prerequisite', fakeAsync(() => { + let actualPrerequisite: any; + const expectedPrerequisite: Prerequisite = { id: 1, title: 'title1' }; + const returnedFromService: Prerequisite = { ...expectedPrerequisite }; + prerequisiteService.getPrerequisite(1, 1).subscribe((resp) => (actualPrerequisite = resp)); + + const req = httpTestingController.expectOne({ method: 'GET' }); + req.flush(returnedFromService); + tick(); + + expect(actualPrerequisite).toEqual(expectedPrerequisite); + })); + it('should convert response dtos to to prerequisite', () => { const expectedPrerequisite1: Prerequisite = { id: 1, title: 'title1', linkedCourseCompetency: { id: 1, course: { id: 1, title: '', semester: 'SS01' } } }; const prerequisiteDTO1: PrerequisiteResponseDTO = { From 44357ddbdd539467e00c41b956d972655c0b0f50 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Mon, 10 Jun 2024 00:58:52 +0200 Subject: [PATCH 68/78] Fix a translation --- .../prerequisite-form/prerequisite-form.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.html b/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.html index 74b00708b6ec..e47494f6f447 100644 --- a/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.html +++ b/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.html @@ -36,7 +36,7 @@
From 9d65c34a0644af35d9445559d880f9154bd784e7 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Mon, 10 Jun 2024 01:29:00 +0200 Subject: [PATCH 69/78] Add navbar translation --- src/main/webapp/app/shared/layouts/navbar/navbar.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts b/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts index 4373cd1735be..0d2f2e09aee5 100644 --- a/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts +++ b/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts @@ -375,6 +375,7 @@ export class NavbarComponent implements OnInit, OnDestroy { commit_details: 'artemisApp.repository.commitHistory.commitDetails.title', repository: 'artemisApp.repository.title', standardized_competencies: 'artemisApp.standardizedCompetency.manage.title', + prerequisites: 'artemisApp.prerequisite.title', import_standardized: 'artemisApp.standardizedCompetency.courseImport.title', }; From decf056eb472dee310cef677f92cd9e77523eb3c Mon Sep 17 00:00:00 2001 From: Raphael Stief <118574504+rstief@users.noreply.github.com> Date: Mon, 10 Jun 2024 12:30:38 +0200 Subject: [PATCH 70/78] Update src/main/webapp/i18n/en/competency.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Johannes Stöhr <38322605+JohannesStoehr@users.noreply.github.com> --- src/main/webapp/i18n/en/competency.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/i18n/en/competency.json b/src/main/webapp/i18n/en/competency.json index ee1a325be11e..c969879a76db 100644 --- a/src/main/webapp/i18n/en/competency.json +++ b/src/main/webapp/i18n/en/competency.json @@ -191,7 +191,7 @@ }, "courseCompetency": { "title": "Title", - "course": "Kurs", + "course": "Course", "description": "Description", "taxonomy": "Taxonomy", "softDueDate": "Recommended until" From a7c901990f8bacafd8171c68a3be8a592a100a3e Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Mon, 10 Jun 2024 12:40:38 +0200 Subject: [PATCH 71/78] Fix navigation bug --- .../prerequisite-form/create-prerequisite.component.ts | 9 ++++----- .../prerequisite-form/edit-prerequisite.component.ts | 9 ++++----- .../prerequisite-form/prerequisite-form.component.html | 2 +- .../competencies/create-prerequisite.component.spec.ts | 10 ++++------ .../competencies/edit-prerequisite.component.spec.ts | 10 ++++------ 5 files changed, 17 insertions(+), 23 deletions(-) diff --git a/src/main/webapp/app/course/competencies/prerequisite-form/create-prerequisite.component.ts b/src/main/webapp/app/course/competencies/prerequisite-form/create-prerequisite.component.ts index fe73ac99bc23..5deb91c7d29f 100644 --- a/src/main/webapp/app/course/competencies/prerequisite-form/create-prerequisite.component.ts +++ b/src/main/webapp/app/course/competencies/prerequisite-form/create-prerequisite.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit, inject } from '@angular/core'; import { onError } from 'app/shared/util/global.utils'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { AlertService } from 'app/core/util/alert.service'; import { finalize } from 'rxjs/operators'; import { HttpErrorResponse } from '@angular/common/http'; @@ -8,7 +8,6 @@ import { Prerequisite } from 'app/entities/prerequisite.model'; import { PrerequisiteService } from 'app/course/competencies/prerequisite.service'; import { PrerequisiteFormComponent } from 'app/course/competencies/prerequisite-form/prerequisite-form.component'; import { ArtemisSharedModule } from 'app/shared/shared.module'; -import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; @Component({ selector: 'jhi-create-prerequisite', @@ -23,7 +22,7 @@ export class CreatePrerequisiteComponent implements OnInit { private readonly activatedRoute: ActivatedRoute = inject(ActivatedRoute); private readonly alertService: AlertService = inject(AlertService); private readonly prerequisiteService: PrerequisiteService = inject(PrerequisiteService); - private readonly navigationUtilService: ArtemisNavigationUtilService = inject(ArtemisNavigationUtilService); + private readonly router: Router = inject(Router); ngOnInit(): void { this.isLoading = true; @@ -44,13 +43,13 @@ export class CreatePrerequisiteComponent implements OnInit { ) .subscribe({ next: () => { - this.navigationUtilService.navigateBack(['course-management', this.courseId, 'competency-management']); + this.router.navigate(['course-management', this.courseId, 'competency-management']); }, error: (res: HttpErrorResponse) => onError(this.alertService, res), }); } cancel() { - this.navigationUtilService.navigateBack(['course-management', this.courseId, 'competency-management']); + this.router.navigate(['course-management', this.courseId, 'competency-management']); } } diff --git a/src/main/webapp/app/course/competencies/prerequisite-form/edit-prerequisite.component.ts b/src/main/webapp/app/course/competencies/prerequisite-form/edit-prerequisite.component.ts index 6d6b4caa7532..c10b8e73c239 100644 --- a/src/main/webapp/app/course/competencies/prerequisite-form/edit-prerequisite.component.ts +++ b/src/main/webapp/app/course/competencies/prerequisite-form/edit-prerequisite.component.ts @@ -7,8 +7,7 @@ import { Prerequisite } from 'app/entities/prerequisite.model'; import { PrerequisiteService } from 'app/course/competencies/prerequisite.service'; import { PrerequisiteFormComponent } from 'app/course/competencies/prerequisite-form/prerequisite-form.component'; import { ArtemisSharedModule } from 'app/shared/shared.module'; -import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; @Component({ selector: 'jhi-edit-prerequisite', @@ -24,7 +23,7 @@ export class EditPrerequisiteComponent implements OnInit { private readonly activatedRoute: ActivatedRoute = inject(ActivatedRoute); private readonly alertService: AlertService = inject(AlertService); private readonly prerequisiteService: PrerequisiteService = inject(PrerequisiteService); - private readonly navigationUtilService: ArtemisNavigationUtilService = inject(ArtemisNavigationUtilService); + private readonly router: Router = inject(Router); ngOnInit(): void { this.isLoading = true; @@ -56,13 +55,13 @@ export class EditPrerequisiteComponent implements OnInit { ) .subscribe({ next: () => { - this.navigationUtilService.navigateBack(['course-management', this.courseId, 'competency-management']); + this.router.navigate(['course-management', this.courseId, 'competency-management']); }, error: (res: HttpErrorResponse) => onError(this.alertService, res), }); } cancel() { - this.navigationUtilService.navigateBack(['course-management', this.courseId, 'competency-management']); + this.router.navigate(['course-management', this.courseId, 'competency-management']); } } diff --git a/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.html b/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.html index e47494f6f447..c3e701cedd54 100644 --- a/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.html +++ b/src/main/webapp/app/course/competencies/prerequisite-form/prerequisite-form.component.html @@ -1,6 +1,6 @@
@if (form) { -
+
diff --git a/src/test/javascript/spec/component/competencies/create-prerequisite.component.spec.ts b/src/test/javascript/spec/component/competencies/create-prerequisite.component.spec.ts index d74cce6b22fc..c2313ce66f83 100644 --- a/src/test/javascript/spec/component/competencies/create-prerequisite.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/create-prerequisite.component.spec.ts @@ -6,7 +6,6 @@ import { of, throwError } from 'rxjs'; import { MockRouter } from '../../helpers/mocks/mock-router'; import { CreatePrerequisiteComponent } from 'app/course/competencies/prerequisite-form/create-prerequisite.component'; import { PrerequisiteService } from 'app/course/competencies/prerequisite.service'; -import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; import { MockTranslateService } from '../../helpers/mocks/service/mock-translate.service'; import { TranslateService } from '@ngx-translate/core'; import { Prerequisite } from 'app/entities/prerequisite.model'; @@ -37,7 +36,6 @@ describe('CreatePrerequisiteComponent', () => { providers: [ provideHttpClient(), MockProvider(PrerequisiteService), - MockProvider(ArtemisNavigationUtilService), MockProvider(AlertService), { provide: TranslateService, @@ -77,8 +75,8 @@ describe('CreatePrerequisiteComponent', () => { it('should navigate back after creating prerequisite', () => { const createSpy = jest.spyOn(prerequisiteService, 'createPrerequisite').mockReturnValue(of(prerequisite)); - const navigationUtilService = TestBed.inject(ArtemisNavigationUtilService); - const navigateSpy = jest.spyOn(navigationUtilService, 'navigateBack'); + const router = TestBed.inject(Router); + const navigateSpy = jest.spyOn(router, 'navigate'); componentFixture.detectChanges(); const prerequisiteForm: PrerequisiteFormStubComponent = componentFixture.debugElement.query(By.directive(PrerequisiteFormStubComponent)).componentInstance; @@ -89,8 +87,8 @@ describe('CreatePrerequisiteComponent', () => { }); it('should navigate on cancel', () => { - const navigationUtilService = TestBed.inject(ArtemisNavigationUtilService); - const navigateSpy = jest.spyOn(navigationUtilService, 'navigateBack'); + const router = TestBed.inject(Router); + const navigateSpy = jest.spyOn(router, 'navigate'); componentFixture.detectChanges(); const prerequisiteForm: PrerequisiteFormStubComponent = componentFixture.debugElement.query(By.directive(PrerequisiteFormStubComponent)).componentInstance; diff --git a/src/test/javascript/spec/component/competencies/edit-prerequisite.component.spec.ts b/src/test/javascript/spec/component/competencies/edit-prerequisite.component.spec.ts index f8dcd0e7dbf5..b852e20dd4ba 100644 --- a/src/test/javascript/spec/component/competencies/edit-prerequisite.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/edit-prerequisite.component.spec.ts @@ -6,7 +6,6 @@ import { of, throwError } from 'rxjs'; import { MockRouter } from '../../helpers/mocks/mock-router'; import { EditPrerequisiteComponent } from 'app/course/competencies/prerequisite-form/edit-prerequisite.component'; import { PrerequisiteService } from 'app/course/competencies/prerequisite.service'; -import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; import { MockTranslateService } from '../../helpers/mocks/service/mock-translate.service'; import { TranslateService } from '@ngx-translate/core'; import { Prerequisite } from 'app/entities/prerequisite.model'; @@ -38,7 +37,6 @@ describe('EditPrerequisiteComponent', () => { providers: [ provideHttpClient(), MockProvider(PrerequisiteService), - MockProvider(ArtemisNavigationUtilService), MockProvider(AlertService), { provide: TranslateService, @@ -85,8 +83,8 @@ describe('EditPrerequisiteComponent', () => { }); it('should navigate back after updating prerequisite', () => { - const navigationUtilService = TestBed.inject(ArtemisNavigationUtilService); - const navigateSpy = jest.spyOn(navigationUtilService, 'navigateBack'); + const router = TestBed.inject(Router); + const navigateSpy = jest.spyOn(router, 'navigate'); const updatedPrerequisite: Prerequisite = { ...prerequisite, title: 'new title', description: 'new description' }; const updateSpy = jest.spyOn(prerequisiteService, 'updatePrerequisite').mockReturnValue(of(updatedPrerequisite)); @@ -99,8 +97,8 @@ describe('EditPrerequisiteComponent', () => { }); it('should navigate on cancel', () => { - const navigationUtilService = TestBed.inject(ArtemisNavigationUtilService); - const navigateSpy = jest.spyOn(navigationUtilService, 'navigateBack'); + const router = TestBed.inject(Router); + const navigateSpy = jest.spyOn(router, 'navigate'); componentFixture.detectChanges(); const prerequisiteForm: PrerequisiteFormStubComponent = componentFixture.debugElement.query(By.directive(PrerequisiteFormStubComponent)).componentInstance; From 70ca81a4df048626a229735c9430a4956186add2 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Tue, 11 Jun 2024 11:32:56 +0200 Subject: [PATCH 72/78] Add javadocs, extract DTO --- .../www1/artemis/domain/competency/Prerequisite.java | 3 +++ .../dto/competency/LinkedCourseCompetencyDTO.java | 12 ++++++++++++ .../rest/dto/competency/PrerequisiteResponseDTO.java | 4 ---- .../artemis/competency/PrerequisiteUtilService.java | 3 +++ 4 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/LinkedCourseCompetencyDTO.java 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 498cfd68c1f1..a8f7c214e170 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 @@ -5,6 +5,9 @@ import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; +/** + * A competency that students are expected to have mastered before participating in a course. + */ @Entity @DiscriminatorValue("P") public class Prerequisite extends CourseCompetency { diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/LinkedCourseCompetencyDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/LinkedCourseCompetencyDTO.java new file mode 100644 index 000000000000..6f2c7fcb5dcf --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/LinkedCourseCompetencyDTO.java @@ -0,0 +1,12 @@ +package de.tum.in.www1.artemis.web.rest.dto.competency; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * A DTO containing the information of the linkedCourseCompetency field of a + * {@link de.tum.in.www1.artemis.domain.competency.CourseCompetency CourseCompetency} + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +record LinkedCourseCompetencyDTO(long id, long courseId, String courseTitle, String semester) { + +} diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteResponseDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteResponseDTO.java index b8e801cb903b..58deca7d984c 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteResponseDTO.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/competency/PrerequisiteResponseDTO.java @@ -32,8 +32,4 @@ public static PrerequisiteResponseDTO of(Prerequisite prerequisite) { prerequisite.getMasteryThreshold(), prerequisite.isOptional(), linkedCourseCompetencyDTO); } - @JsonInclude(JsonInclude.Include.NON_EMPTY) - private record LinkedCourseCompetencyDTO(long id, long courseId, String courseTitle, String semester) { - - } } diff --git a/src/test/java/de/tum/in/www1/artemis/competency/PrerequisiteUtilService.java b/src/test/java/de/tum/in/www1/artemis/competency/PrerequisiteUtilService.java index c7c4579e68d2..6deb83311d4b 100644 --- a/src/test/java/de/tum/in/www1/artemis/competency/PrerequisiteUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/competency/PrerequisiteUtilService.java @@ -10,6 +10,9 @@ import de.tum.in.www1.artemis.domain.competency.Prerequisite; import de.tum.in.www1.artemis.repository.PrerequisiteRepository; +/** + * Service responsible for initializing the database with specific test data related to prerequisites for use in integration tests. + */ @Service public class PrerequisiteUtilService { From a5b4a09cdade66bf3a01cd5ada659d3b81ad47d6 Mon Sep 17 00:00:00 2001 From: Raphael Stief <118574504+rstief@users.noreply.github.com> Date: Tue, 11 Jun 2024 19:57:54 +0200 Subject: [PATCH 73/78] Update src/main/webapp/app/course/competencies/import-competencies/import-course-competencies.component.html --- .../import-course-competencies.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/course/competencies/import-competencies/import-course-competencies.component.html b/src/main/webapp/app/course/competencies/import-competencies/import-course-competencies.component.html index e0e3c59cb215..05d856b94713 100644 --- a/src/main/webapp/app/course/competencies/import-competencies/import-course-competencies.component.html +++ b/src/main/webapp/app/course/competencies/import-competencies/import-course-competencies.component.html @@ -9,7 +9,7 @@

< } @else { - + }

@if (selectedCourseCompetencies.resultsOnPage?.length) { From 5f4ea0c137748bb3beac10e6f907889be162daaa Mon Sep 17 00:00:00 2001 From: Raphael Stief <118574504+rstief@users.noreply.github.com> Date: Sat, 22 Jun 2024 20:38:12 +0200 Subject: [PATCH 74/78] Update src/test/java/de/tum/in/www1/artemis/competency/PrerequisiteUtilService.java Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../tum/in/www1/artemis/competency/PrerequisiteUtilService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/de/tum/in/www1/artemis/competency/PrerequisiteUtilService.java b/src/test/java/de/tum/in/www1/artemis/competency/PrerequisiteUtilService.java index a500b339af99..0f00e7ce6e2a 100644 --- a/src/test/java/de/tum/in/www1/artemis/competency/PrerequisiteUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/competency/PrerequisiteUtilService.java @@ -67,7 +67,7 @@ public List createPrerequisites(Course course, int numberOfPrerequ /** * Creates a PrerequisiteRequestDTO from a prerequisite * - * @param prerequisite the prerequisite to conver + * @param prerequisite the prerequisite to convert * @return the created PrerequisiteRequestDTO */ public PrerequisiteRequestDTO prerequisiteToRequestDTO(Prerequisite prerequisite) { From 02a1970eb54e55e94477a37d0ae4e9b9d89b2a37 Mon Sep 17 00:00:00 2001 From: Raphael Stief Date: Sat, 22 Jun 2024 21:29:57 +0200 Subject: [PATCH 75/78] Fix style issues --- .../in/www1/artemis/lecture/CompetencyIntegrationTest.java | 2 +- .../component/competencies/competency-form.component.spec.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 027d87cf4195..46a8abc708fa 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 @@ -10,8 +10,8 @@ import java.util.List; import java.util.Set; import java.util.stream.Collectors; -import java.util.stream.Stream; import java.util.stream.IntStream; +import java.util.stream.Stream; import org.glassfish.jersey.internal.util.Producer; import org.junit.jupiter.api.BeforeEach; diff --git a/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts b/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts index 1cdcba151f46..f7186c944a44 100644 --- a/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts @@ -4,7 +4,7 @@ import { ReactiveFormsModule } from '@angular/forms'; import { NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { CompetencyFormComponent, CompetencyFormData } from 'app/course/competencies/competency-form/competency-form.component'; import { CompetencyService } from 'app/course/competencies/competency.service'; -import { CompetencyTaxonomy } from 'app/entities/competency.model'; +import { Competency, CompetencyTaxonomy } from 'app/entities/competency.model'; import { TextUnit } from 'app/entities/lecture-unit/textUnit.model'; import { Lecture } from 'app/entities/lecture.model'; import { LectureUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/lectureUnit.service'; @@ -70,7 +70,7 @@ describe('CompetencyFormComponent', () => { status: 200, }); - const getAllForCourseSpy = jest.spyOn(competencyService, 'getAllForCourse').mockReturnValue(of(response)); + jest.spyOn(competencyService, 'getAllForCourse').mockReturnValue(of(response)); competencyFormComponentFixture.detectChanges(); From a0c274284687bf388b73ed167c80f77d43a57aea Mon Sep 17 00:00:00 2001 From: Raphael Stief <118574504+rstief@users.noreply.github.com> Date: Sun, 23 Jun 2024 13:18:12 +0200 Subject: [PATCH 76/78] Apply suggestions from code review Co-authored-by: Jan Thurner <107639007+Jan-Thurner@users.noreply.github.com> --- .../competencies/competency-form.component.spec.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts b/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts index f7186c944a44..888b416104a1 100644 --- a/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts @@ -61,8 +61,10 @@ describe('CompetencyFormComponent', () => { const competencyService = TestBed.inject(CompetencyService); const getAllTitlesSpy = jest.spyOn(competencyService, 'getCourseCompetencyTitles').mockReturnValue(of(new HttpResponse({ body: ['test'], status: 200 }))); - const competencyOfResponse: Competency = {}; - competencyOfResponse.id = 1; + const competencyOfResponse: Competency = { + id: 1, + title: 'test', + }; competencyOfResponse.title = 'test'; const response: HttpResponse = new HttpResponse({ From d3453ae87db9caefb94b49d1719814252366c8a3 Mon Sep 17 00:00:00 2001 From: Raphael Stief <118574504+rstief@users.noreply.github.com> Date: Sun, 23 Jun 2024 13:18:32 +0200 Subject: [PATCH 77/78] Update src/test/javascript/spec/component/competencies/competency-form.component.spec.ts Co-authored-by: Jan Thurner <107639007+Jan-Thurner@users.noreply.github.com> --- .../component/competencies/competency-form.component.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts b/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts index 888b416104a1..0be69ea05863 100644 --- a/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts @@ -65,7 +65,6 @@ describe('CompetencyFormComponent', () => { id: 1, title: 'test', }; - competencyOfResponse.title = 'test'; const response: HttpResponse = new HttpResponse({ body: [competencyOfResponse], From 53832f0e9f7fdb73a25289c15ed9056122e9e5ae Mon Sep 17 00:00:00 2001 From: Raphael Stief <118574504+rstief@users.noreply.github.com> Date: Sun, 23 Jun 2024 23:40:56 +0200 Subject: [PATCH 78/78] Update src/test/javascript/spec/component/competencies/competency-form.component.spec.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../component/competencies/competency-form.component.spec.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts b/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts index 0be69ea05863..0204462728f8 100644 --- a/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts @@ -61,10 +61,7 @@ describe('CompetencyFormComponent', () => { const competencyService = TestBed.inject(CompetencyService); const getAllTitlesSpy = jest.spyOn(competencyService, 'getCourseCompetencyTitles').mockReturnValue(of(new HttpResponse({ body: ['test'], status: 200 }))); - const competencyOfResponse: Competency = { - id: 1, - title: 'test', - }; + const competencyOfResponse: Competency = { id: 1, title: 'test' }; const response: HttpResponse = new HttpResponse({ body: [competencyOfResponse],