getExamUsers() {
diff --git a/src/main/java/de/tum/in/www1/artemis/domain/VcsRepositoryUrl.java b/src/main/java/de/tum/in/www1/artemis/domain/VcsRepositoryUrl.java
index 46555c8e0f52..77cdc1a67112 100644
--- a/src/main/java/de/tum/in/www1/artemis/domain/VcsRepositoryUrl.java
+++ b/src/main/java/de/tum/in/www1/artemis/domain/VcsRepositoryUrl.java
@@ -66,7 +66,7 @@ public String toString() {
* Generates the unique local folder name for a given file or remote repository URI.
* For file URIs, we take the last element of the path, which should be unique
* For URLs pointing to remote git repositories, we use the whole path
- *
+ *
* Examples:
* https://bitbucket.ase.in.tum.de/scm/eist20l06e03/eist20l06e03-ab123cd.git --> eist20l06e03/eist20l06e03-ab123cd
* ssh://git@bitbucket.ase.in.tum.de:7999/eist20l06e03/eist20l06e03-ab123cd.git --> eist20l06e03/eist20l06e03-ab123cd
@@ -86,6 +86,7 @@ public String folderNameForRepositoryUrl() {
path = path.replaceAll(".git$", "");
path = path.replaceAll("/$", "");
path = path.replaceAll("^/.*scm", "");
+ path = path.replaceAll("^/.*git", "");
return path;
}
}
diff --git a/src/main/java/de/tum/in/www1/artemis/domain/enumeration/DefaultChannelType.java b/src/main/java/de/tum/in/www1/artemis/domain/enumeration/DefaultChannelType.java
new file mode 100644
index 000000000000..0595b3cf7489
--- /dev/null
+++ b/src/main/java/de/tum/in/www1/artemis/domain/enumeration/DefaultChannelType.java
@@ -0,0 +1,19 @@
+package de.tum.in.www1.artemis.domain.enumeration;
+
+/**
+ * Enumeration for default channel types that are automatically created on course creation
+ */
+public enum DefaultChannelType {
+
+ ANNOUNCEMENT("announcement"), ORGANIZATION("organization"), RANDOM("random"), TECH_SUPPORT("tech-support"),;
+
+ private final String name;
+
+ DefaultChannelType(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/src/main/java/de/tum/in/www1/artemis/domain/lecture/ExerciseUnit.java b/src/main/java/de/tum/in/www1/artemis/domain/lecture/ExerciseUnit.java
index 41d4f0dd8bbb..486188f89012 100644
--- a/src/main/java/de/tum/in/www1/artemis/domain/lecture/ExerciseUnit.java
+++ b/src/main/java/de/tum/in/www1/artemis/domain/lecture/ExerciseUnit.java
@@ -13,8 +13,8 @@
import com.fasterxml.jackson.annotation.JsonInclude;
+import de.tum.in.www1.artemis.domain.Competency;
import de.tum.in.www1.artemis.domain.Exercise;
-import de.tum.in.www1.artemis.domain.LearningGoal;
@Entity
@DiscriminatorValue("E")
@@ -61,12 +61,12 @@ public void setReleaseDate(ZonedDateTime releaseDate) {
}
@Override
- public Set getLearningGoals() {
- return exercise == null || !Hibernate.isPropertyInitialized(exercise, "learningGoals") ? new HashSet<>() : exercise.getLearningGoals();
+ public Set getCompetencies() {
+ return exercise == null || !Hibernate.isPropertyInitialized(exercise, "competencies") ? new HashSet<>() : exercise.getCompetencies();
}
@Override
- public void setLearningGoals(Set learningGoals) {
+ public void setCompetencies(Set competencies) {
// Should be set in associated exercise
}
@@ -78,6 +78,6 @@ public void setLearningGoals(Set learningGoals) {
public void prePersistOrUpdate() {
this.name = null;
this.releaseDate = null;
- this.learningGoals = new HashSet<>();
+ this.competencies = new HashSet<>();
}
}
diff --git a/src/main/java/de/tum/in/www1/artemis/domain/lecture/LectureUnit.java b/src/main/java/de/tum/in/www1/artemis/domain/lecture/LectureUnit.java
index 84bfe25fff51..788b785891ef 100644
--- a/src/main/java/de/tum/in/www1/artemis/domain/lecture/LectureUnit.java
+++ b/src/main/java/de/tum/in/www1/artemis/domain/lecture/LectureUnit.java
@@ -53,7 +53,7 @@ public abstract class LectureUnit extends DomainObject implements LearningObject
@OrderBy("title")
@JsonIgnoreProperties({ "lectureUnits", "course" })
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
- protected Set learningGoals = new HashSet<>();
+ protected Set competencies = new HashSet<>();
@OneToMany(mappedBy = "lectureUnit", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE, orphanRemoval = true)
@JsonIgnore // important, so that the completion status of other users do not leak to anyone
@@ -87,12 +87,12 @@ public void setReleaseDate(ZonedDateTime releaseDate) {
this.releaseDate = releaseDate;
}
- public Set getLearningGoals() {
- return learningGoals;
+ public Set getCompetencies() {
+ return competencies;
}
- public void setLearningGoals(Set learningGoals) {
- this.learningGoals = learningGoals;
+ public void setCompetencies(Set competencies) {
+ this.competencies = competencies;
}
@JsonIgnore(false)
diff --git a/src/main/java/de/tum/in/www1/artemis/exception/LocalCIException.java b/src/main/java/de/tum/in/www1/artemis/exception/LocalCIException.java
new file mode 100644
index 000000000000..c163d17a1035
--- /dev/null
+++ b/src/main/java/de/tum/in/www1/artemis/exception/LocalCIException.java
@@ -0,0 +1,16 @@
+package de.tum.in.www1.artemis.exception;
+
+/**
+ * Exception thrown when something goes wrong with the local CI system.
+ * This is an unchecked exception and should only be used, if the error is not recoverable.
+ */
+public class LocalCIException extends ContinuousIntegrationException {
+
+ public LocalCIException(String message) {
+ super(message);
+ }
+
+ public LocalCIException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/de/tum/in/www1/artemis/exception/localvc/LocalVCAuthException.java b/src/main/java/de/tum/in/www1/artemis/exception/localvc/LocalVCAuthException.java
new file mode 100644
index 000000000000..4ef109ef53cb
--- /dev/null
+++ b/src/main/java/de/tum/in/www1/artemis/exception/localvc/LocalVCAuthException.java
@@ -0,0 +1,16 @@
+package de.tum.in.www1.artemis.exception.localvc;
+
+/**
+ * Exception thrown when the user is not authenticated or authorized to fetch or push to a local VC repository.
+ * Corresponds to HTTP status code 401.
+ */
+public class LocalVCAuthException extends LocalVCOperationException {
+
+ public LocalVCAuthException() {
+ // empty constructor
+ }
+
+ public LocalVCAuthException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/de/tum/in/www1/artemis/exception/localvc/LocalVCForbiddenException.java b/src/main/java/de/tum/in/www1/artemis/exception/localvc/LocalVCForbiddenException.java
new file mode 100644
index 000000000000..c24206d8bb65
--- /dev/null
+++ b/src/main/java/de/tum/in/www1/artemis/exception/localvc/LocalVCForbiddenException.java
@@ -0,0 +1,16 @@
+package de.tum.in.www1.artemis.exception.localvc;
+
+/**
+ * Exception thrown when the user is authorized but not allowed to fetch or push to a local VC repository.
+ * Corresponds to HTTP status code 403.
+ */
+public class LocalVCForbiddenException extends LocalVCOperationException {
+
+ public LocalVCForbiddenException() {
+ // empty constructor
+ }
+
+ public LocalVCForbiddenException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/de/tum/in/www1/artemis/exception/localvc/LocalVCInternalException.java b/src/main/java/de/tum/in/www1/artemis/exception/localvc/LocalVCInternalException.java
new file mode 100644
index 000000000000..49307c684218
--- /dev/null
+++ b/src/main/java/de/tum/in/www1/artemis/exception/localvc/LocalVCInternalException.java
@@ -0,0 +1,18 @@
+package de.tum.in.www1.artemis.exception.localvc;
+
+import de.tum.in.www1.artemis.exception.VersionControlException;
+
+/**
+ * Exception thrown when an internal error occurs in the local version control system.
+ * Corresponds to HTTP status code 500.
+ */
+public class LocalVCInternalException extends VersionControlException {
+
+ public LocalVCInternalException(String message) {
+ super(message);
+ }
+
+ public LocalVCInternalException(String message, Exception e) {
+ super(message, e);
+ }
+}
diff --git a/src/main/java/de/tum/in/www1/artemis/exception/localvc/LocalVCOperationException.java b/src/main/java/de/tum/in/www1/artemis/exception/localvc/LocalVCOperationException.java
new file mode 100644
index 000000000000..beb7ae5b3edc
--- /dev/null
+++ b/src/main/java/de/tum/in/www1/artemis/exception/localvc/LocalVCOperationException.java
@@ -0,0 +1,15 @@
+package de.tum.in.www1.artemis.exception.localvc;
+
+/**
+ * Generic exception for all local version control purposes.
+ */
+public class LocalVCOperationException extends Exception {
+
+ public LocalVCOperationException() {
+ // empty constructor
+ }
+
+ public LocalVCOperationException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/de/tum/in/www1/artemis/repository/CompetencyProgressRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/CompetencyProgressRepository.java
new file mode 100644
index 000000000000..f06525a98ec7
--- /dev/null
+++ b/src/main/java/de/tum/in/www1/artemis/repository/CompetencyProgressRepository.java
@@ -0,0 +1,76 @@
+package de.tum.in.www1.artemis.repository;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import de.tum.in.www1.artemis.domain.CompetencyProgress;
+
+@Repository
+public interface CompetencyProgressRepository extends JpaRepository {
+
+ @Transactional // ok because of delete
+ @Modifying
+ @Query("DELETE FROM CompetencyProgress cp WHERE cp.learningGoal.id = :learningGoalId")
+ void deleteAllByCompetencyId(Long learningGoalId);
+
+ @Transactional // ok because of delete
+ @Modifying
+ void deleteAllByUserId(Long userId);
+
+ @Query("""
+ SELECT cp
+ FROM CompetencyProgress cp
+ WHERE cp.learningGoal.id = :competencyId
+ """)
+ List findAllByCompetencyId(@Param("competencyId") Long competencyId);
+
+ @Query("""
+ SELECT cp
+ FROM CompetencyProgress cp
+ WHERE cp.learningGoal.id = :competencyId
+ AND cp.user.id = :userId
+ """)
+ Optional findByCompetencyIdAndUserId(@Param("competencyId") Long competencyId, @Param("userId") Long userId);
+
+ @Query("""
+ SELECT cp
+ FROM CompetencyProgress cp
+ LEFT JOIN FETCH cp.user
+ LEFT JOIN FETCH cp.learningGoal
+ WHERE cp.learningGoal.id = :competencyId
+ AND cp.user.id = :userId
+ """)
+ Optional findEagerByCompetencyIdAndUserId(@Param("competencyId") Long competencyId, @Param("userId") Long userId);
+
+ @Query("""
+ SELECT AVG(cp.confidence)
+ FROM CompetencyProgress cp
+ WHERE cp.learningGoal.id = :competencyId
+ """)
+ Optional findAverageConfidenceByCompetencyId(@Param("competencyId") Long competencyId);
+
+ @Query("""
+ SELECT count(cp)
+ FROM CompetencyProgress cp
+ WHERE cp.learningGoal.id = :competencyId
+ """)
+ Long countByCompetency(@Param("competencyId") Long competencyId);
+
+ @Query("""
+ SELECT count(cp)
+ FROM CompetencyProgress cp
+ WHERE cp.learningGoal.id = :competencyId
+ AND cp.progress >= :progress
+ AND cp.confidence >= :confidence
+ """)
+ Long countByCompetencyAndProgressAndConfidenceGreaterThanEqual(@Param("competencyId") Long competencyId, @Param("progress") Double progress,
+ @Param("confidence") Double confidence);
+
+}
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
new file mode 100644
index 000000000000..308e854f1922
--- /dev/null
+++ b/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRelationRepository.java
@@ -0,0 +1,36 @@
+package de.tum.in.www1.artemis.repository;
+
+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 org.springframework.stereotype.Repository;
+
+import de.tum.in.www1.artemis.domain.CompetencyRelation;
+
+/**
+ * Spring Data JPA repository for the Competency Relation entity.
+ */
+@Repository
+public interface CompetencyRelationRepository extends JpaRepository {
+
+ @Query("""
+ SELECT relation
+ FROM CompetencyRelation relation
+ WHERE relation.headCompetency.id = :#{#competencyId}
+ OR relation.tailCompetency.id = :#{#competencyId}
+ """)
+ Set findAllByCompetencyId(@Param("competencyId") Long competencyId);
+
+ @Query("""
+ SELECT relation
+ FROM CompetencyRelation relation
+ LEFT JOIN FETCH relation.headCompetency
+ LEFT JOIN FETCH relation.tailCompetency
+ WHERE relation.headCompetency.course.id = :#{#courseId}
+ AND relation.tailCompetency.course.id = :#{#courseId}
+ """)
+ Set findAllByCourseId(@Param("courseId") Long courseId);
+
+}
diff --git a/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRepository.java
new file mode 100644
index 000000000000..336e63e66bd0
--- /dev/null
+++ b/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRepository.java
@@ -0,0 +1,183 @@
+package de.tum.in.www1.artemis.repository;
+
+import java.util.Optional;
+import java.util.Set;
+
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.stereotype.Repository;
+
+import de.tum.in.www1.artemis.domain.Competency;
+import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException;
+
+/**
+ * Spring Data JPA repository for the Competency entity.
+ */
+@Repository
+public interface CompetencyRepository extends JpaRepository {
+
+ @Query("""
+ SELECT c
+ FROM Competency c
+ LEFT JOIN FETCH c.userProgress progress
+ WHERE c.course.id = :courseId
+ """)
+ Set findAllForCourse(@Param("courseId") Long courseId);
+
+ @Query("""
+ SELECT c
+ FROM Competency c
+ LEFT JOIN FETCH c.userProgress progress
+ WHERE c.course.id = :courseId
+ AND (progress IS NULL OR progress.user.id = :userId)
+ """)
+ Set findAllForCourseWithProgressForUser(@Param("courseId") Long courseId, @Param("userId") Long userId);
+
+ @Query("""
+ SELECT c
+ FROM Competency c
+ LEFT JOIN FETCH c.exercises ex
+ WHERE c.id = :#{#competencyId}
+ """)
+ Optional findByIdWithExercises(@Param("competencyId") long competencyId);
+
+ @Query("""
+ SELECT c
+ FROM Competency c
+ LEFT JOIN FETCH c.lectureUnits lu
+ WHERE c.id = :#{#competencyId}
+ """)
+ Optional findByIdWithLectureUnits(@Param("competencyId") long competencyId);
+
+ @Query("""
+ SELECT c
+ FROM Competency c
+ LEFT JOIN FETCH c.userProgress
+ LEFT JOIN FETCH c.exercises
+ LEFT JOIN FETCH c.lectureUnits lu
+ LEFT JOIN FETCH lu.completedUsers
+ LEFT JOIN FETCH lu.lecture l
+ LEFT JOIN FETCH lu.exercise e
+ WHERE c.id = :competencyId
+ """)
+ Optional findByIdWithExercisesAndLectureUnits(@Param("competencyId") Long competencyId);
+
+ @Query("""
+ SELECT c
+ FROM Competency c
+ LEFT JOIN FETCH c.lectureUnits lu
+ LEFT JOIN FETCH lu.completedUsers
+ WHERE c.id = :competencyId
+ """)
+ Optional findByIdWithLectureUnitsAndCompletions(@Param("competencyId") Long competencyId);
+
+ @Query("""
+ SELECT c
+ FROM Competency c
+ LEFT JOIN FETCH c.exercises
+ LEFT JOIN FETCH c.lectureUnits lu
+ LEFT JOIN FETCH lu.completedUsers
+ WHERE c.id = :competencyId
+ """)
+ Optional findByIdWithExercisesAndLectureUnitsAndCompletions(@Param("competencyId") Long competencyId);
+
+ @Query("""
+ SELECT c
+ FROM Competency c
+ LEFT JOIN FETCH c.exercises ex
+ LEFT JOIN FETCH ex.competencies
+ LEFT JOIN FETCH c.lectureUnits lu
+ LEFT JOIN FETCH lu.competencies
+ WHERE c.id = :competencyId
+ """)
+ Optional findByIdWithExercisesAndLectureUnitsBidirectional(@Param("competencyId") Long competencyId);
+
+ @Query("""
+ 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 FETCH pr.consecutiveCourses c
+ WHERE c.id = :courseId
+ ORDER BY pr.title
+ """)
+ Set findPrerequisitesByCourseId(@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.
+ *
+ * @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);
+
+ /**
+ * Returns the title of the competency with the given id.
+ *
+ * @param competencyId the id of the competency
+ * @return the name/title of the competency or null if the competency does not exist
+ */
+ @Query("""
+ SELECT c.title
+ FROM Competency c
+ WHERE c.id = :competencyId
+ """)
+ @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));
+ }
+
+ default Competency findByIdWithExercisesAndLectureUnitsAndCompletionsElseThrow(long competencyId) {
+ return findByIdWithExercisesAndLectureUnitsAndCompletions(competencyId).orElseThrow(() -> new EntityNotFoundException("Competency", competencyId));
+ }
+
+ default Competency findByIdWithExercisesAndLectureUnitsBidirectionalElseThrow(long competencyId) {
+ 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));
+ }
+
+ default Competency findByIdWithLectureUnitsElseThrow(Long competencyId) {
+ return findByIdWithLectureUnits(competencyId).orElseThrow(() -> new EntityNotFoundException("Competency", competencyId));
+ }
+
+ default Competency findByIdWithExercisesAndLectureUnitsElseThrow(Long competencyId) {
+ return findByIdWithExercisesAndLectureUnits(competencyId).orElseThrow(() -> new EntityNotFoundException("Competency", competencyId));
+ }
+
+ default Competency findByIdWithExercisesElseThrow(Long competencyId) {
+ return findByIdWithExercises(competencyId).orElseThrow(() -> new EntityNotFoundException("Competency", competencyId));
+ }
+}
diff --git a/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java
index 010589e08b74..c436440984ce 100644
--- a/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java
+++ b/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java
@@ -52,6 +52,27 @@ public interface CourseRepository extends JpaRepository {
@Query("select distinct course from Course course where course.studentGroupName like :name")
Course findCourseByStudentGroupName(@Param("name") String name);
+ @Query("""
+ SELECT DISTINCT course
+ FROM Course course
+ WHERE course.instructorGroupName = :name
+ """)
+ List findCoursesByInstructorGroupName(@Param("name") String name);
+
+ @Query("""
+ SELECT DISTINCT course
+ FROM Course course
+ WHERE course.teachingAssistantGroupName = :name
+ """)
+ List findCoursesByTeachingAssistantGroupName(@Param("name") String name);
+
+ @Query("""
+ SELECT DISTINCT course
+ FROM Course course
+ WHERE course.studentGroupName = :name
+ """)
+ List findCoursesByStudentGroupName(@Param("name") String name);
+
@Query("""
SELECT CASE WHEN (count(c) > 0) THEN true ELSE false END
FROM Course c
@@ -120,8 +141,8 @@ SELECT CASE WHEN (count(c) > 0) THEN true ELSE false END
@EntityGraph(type = LOAD, attributePaths = { "exercises", "exercises.categories", "exercises.teamAssignmentConfig" })
Course findWithEagerExercisesById(long courseId);
- @EntityGraph(type = LOAD, attributePaths = { "learningGoals", "prerequisites" })
- Optional findWithEagerLearningGoalsById(long courseId);
+ @EntityGraph(type = LOAD, attributePaths = { "competencies", "prerequisites" })
+ Optional findWithEagerCompetenciesById(long courseId);
@EntityGraph(type = LOAD, attributePaths = { "lectures" })
Optional findWithEagerLecturesById(long courseId);
@@ -132,11 +153,11 @@ SELECT CASE WHEN (count(c) > 0) THEN true ELSE false END
@EntityGraph(type = LOAD, attributePaths = { "lectures", "lectures.lectureUnits" })
Optional findWithEagerLecturesAndLectureUnitsById(long courseId);
- @EntityGraph(type = LOAD, attributePaths = { "organizations", "learningGoals", "prerequisites", "tutorialGroupsConfiguration", "onlineCourseConfiguration" })
- Optional findWithEagerOrganizationsAndLearningGoalsAndOnlineConfigurationById(long courseId);
+ @EntityGraph(type = LOAD, attributePaths = { "organizations", "competencies", "prerequisites", "tutorialGroupsConfiguration", "onlineCourseConfiguration" })
+ Optional findWithEagerOrganizationsAndCompetenciesAndOnlineConfigurationById(long courseId);
- @EntityGraph(type = LOAD, attributePaths = { "exercises", "lectures", "lectures.lectureUnits", "learningGoals", "prerequisites" })
- Optional findWithEagerExercisesAndLecturesAndLectureUnitsAndLearningGoalsById(long courseId);
+ @EntityGraph(type = LOAD, attributePaths = { "exercises", "lectures", "lectures.lectureUnits", "competencies", "prerequisites" })
+ Optional findWithEagerExercisesAndLecturesAndLectureUnitsAndCompetenciesById(long courseId);
@Query("""
SELECT course
@@ -374,18 +395,18 @@ default Course findByIdWithLecturesAndLectureUnitsElseThrow(long courseId) {
}
@NotNull
- default Course findByIdWithOrganizationsAndLearningGoalsAndOnlineConfigurationElseThrow(long courseId) {
- return findWithEagerOrganizationsAndLearningGoalsAndOnlineConfigurationById(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId));
+ default Course findByIdWithOrganizationsAndCompetenciesAndOnlineConfigurationElseThrow(long courseId) {
+ return findWithEagerOrganizationsAndCompetenciesAndOnlineConfigurationById(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId));
}
@NotNull
- default Course findByIdWithExercisesAndLecturesAndLectureUnitsAndLearningGoalsElseThrow(long courseId) {
- return findWithEagerExercisesAndLecturesAndLectureUnitsAndLearningGoalsById(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId));
+ default Course findByIdWithExercisesAndLecturesAndLectureUnitsAndCompetenciesElseThrow(long courseId) {
+ return findWithEagerExercisesAndLecturesAndLectureUnitsAndCompetenciesById(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId));
}
@NotNull
- default Course findWithEagerLearningGoalsByIdElseThrow(long courseId) {
- return findWithEagerLearningGoalsById(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId));
+ default Course findWithEagerCompetenciesByIdElseThrow(long courseId) {
+ return findWithEagerCompetenciesById(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId));
}
/**
diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ExerciseRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ExerciseRepository.java
index c635e4458689..54e4785d9e98 100644
--- a/src/main/java/de/tum/in/www1/artemis/repository/ExerciseRepository.java
+++ b/src/main/java/de/tum/in/www1/artemis/repository/ExerciseRepository.java
@@ -67,18 +67,18 @@ public interface ExerciseRepository extends JpaRepository {
@Query("""
SELECT e
FROM Exercise e
- LEFT JOIN FETCH e.learningGoals
+ LEFT JOIN FETCH e.competencies
WHERE e.id = :exerciseId
""")
- Optional findByIdWithLearningGoals(@Param("exerciseId") Long exerciseId);
+ Optional findByIdWithCompetencies(@Param("exerciseId") Long exerciseId);
@Query("""
SELECT e FROM Exercise e
- LEFT JOIN FETCH e.learningGoals lg
- LEFT JOIN FETCH lg.exercises
+ LEFT JOIN FETCH e.competencies c
+ LEFT JOIN FETCH c.exercises
WHERE e.id = :exerciseId
""")
- Optional findByIdWithLearningGoalsBidirectional(@Param("exerciseId") Long exerciseId);
+ Optional findByIdWithCompetenciesBidirectional(@Param("exerciseId") Long exerciseId);
@Query("""
SELECT e FROM Exercise e
@@ -388,13 +388,13 @@ AND st.id IN (
* @return all exercises that should be part of the summary (email)
*/
@Query("""
- SELECT exercise
- FROM Exercise exercise
- WHERE exercise.releaseDate IS NOT NULL
- AND exercise.releaseDate < :now
- AND exercise.releaseDate > :daysAgo
- AND ((exercise.dueDate IS NOT NULL AND exercise.dueDate > :now)
- OR exercise.dueDate IS NULL)
+ SELECT e
+ FROM Exercise e
+ WHERE e.releaseDate IS NOT NULL
+ AND e.releaseDate < :now
+ AND e.releaseDate > :daysAgo
+ AND ((e.dueDate IS NOT NULL AND e.dueDate > :now)
+ OR e.dueDate IS NULL)
""")
Set findAllExercisesForSummary(@Param("now") ZonedDateTime now, @Param("daysAgo") ZonedDateTime daysAgo);
@@ -406,7 +406,8 @@ AND st.id IN (
*/
@Query("""
SELECT COUNT(DISTINCT p.student.id)
- FROM Exercise e JOIN e.studentParticipations p
+ FROM Exercise e
+ JOIN e.studentParticipations p
WHERE e.id = :exerciseId
""")
Long getStudentParticipationCountById(@Param("exerciseId") Long exerciseId);
@@ -443,13 +444,13 @@ default Exercise findByIdElseThrow(Long exerciseId) throws EntityNotFoundExcepti
}
@NotNull
- default Exercise findByIdWithLearningGoalsElseThrow(Long exerciseId) throws EntityNotFoundException {
- return findByIdWithLearningGoals(exerciseId).orElseThrow(() -> new EntityNotFoundException("Exercise", exerciseId));
+ default Exercise findByIdWithCompetenciesElseThrow(Long exerciseId) throws EntityNotFoundException {
+ return findByIdWithCompetencies(exerciseId).orElseThrow(() -> new EntityNotFoundException("Exercise", exerciseId));
}
@NotNull
- default Exercise findByIdWithLearningGoalsBidirectionalElseThrow(Long exerciseId) throws EntityNotFoundException {
- return findByIdWithLearningGoalsBidirectional(exerciseId).orElseThrow(() -> new EntityNotFoundException("Exercise", exerciseId));
+ default Exercise findByIdWithCompetenciesBidirectionalElseThrow(Long exerciseId) throws EntityNotFoundException {
+ return findByIdWithCompetenciesBidirectional(exerciseId).orElseThrow(() -> new EntityNotFoundException("Exercise", exerciseId));
}
/**
diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ExerciseUnitRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ExerciseUnitRepository.java
index ccd565b1a0a2..f1998dece3b5 100644
--- a/src/main/java/de/tum/in/www1/artemis/repository/ExerciseUnitRepository.java
+++ b/src/main/java/de/tum/in/www1/artemis/repository/ExerciseUnitRepository.java
@@ -27,9 +27,9 @@ public interface ExerciseUnitRepository extends JpaRepository findByIdWithLearningGoalsBidirectional(@Param("exerciseId") Long exerciseId);
+ List findByIdWithCompetenciesBidirectional(@Param("exerciseId") Long exerciseId);
}
diff --git a/src/main/java/de/tum/in/www1/artemis/repository/FileUploadExerciseRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/FileUploadExerciseRepository.java
index 5bb20ef7ec3b..dad6d1c2678a 100644
--- a/src/main/java/de/tum/in/www1/artemis/repository/FileUploadExerciseRepository.java
+++ b/src/main/java/de/tum/in/www1/artemis/repository/FileUploadExerciseRepository.java
@@ -31,8 +31,8 @@ public interface FileUploadExerciseRepository extends JpaRepository findByCourseIdWithCategories(@Param("courseId") Long courseId);
- @EntityGraph(type = LOAD, attributePaths = { "teamAssignmentConfig", "categories", "learningGoals" })
- Optional findWithEagerTeamAssignmentConfigAndCategoriesAndLearningGoalsById(Long exerciseId);
+ @EntityGraph(type = LOAD, attributePaths = { "teamAssignmentConfig", "categories", "competencies" })
+ Optional findWithEagerTeamAssignmentConfigAndCategoriesAndCompetenciesById(Long exerciseId);
/**
* Get one file upload exercise by id.
diff --git a/src/main/java/de/tum/in/www1/artemis/repository/LearningGoalProgressRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/LearningGoalProgressRepository.java
deleted file mode 100644
index d013488a4815..000000000000
--- a/src/main/java/de/tum/in/www1/artemis/repository/LearningGoalProgressRepository.java
+++ /dev/null
@@ -1,75 +0,0 @@
-package de.tum.in.www1.artemis.repository;
-
-import java.util.List;
-import java.util.Optional;
-
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.Modifying;
-import org.springframework.data.jpa.repository.Query;
-import org.springframework.data.repository.query.Param;
-import org.springframework.stereotype.Repository;
-import org.springframework.transaction.annotation.Transactional;
-
-import de.tum.in.www1.artemis.domain.LearningGoalProgress;
-
-@Repository
-public interface LearningGoalProgressRepository extends JpaRepository {
-
- @Transactional // ok because of delete
- @Modifying
- void deleteAllByLearningGoalId(Long learningGoalId);
-
- @Transactional // ok because of delete
- @Modifying
- void deleteAllByUserId(Long userId);
-
- @Query("""
- SELECT lgp
- FROM LearningGoalProgress lgp
- WHERE lgp.learningGoal.id = :learningGoalId
- """)
- List findAllByLearningGoalId(@Param("learningGoalId") Long learningGoalId);
-
- @Query("""
- SELECT lgp
- FROM LearningGoalProgress lgp
- WHERE lgp.learningGoal.id = :learningGoalId
- AND lgp.user.id = :userId
- """)
- Optional findByLearningGoalIdAndUserId(@Param("learningGoalId") Long learningGoalId, @Param("userId") Long userId);
-
- @Query("""
- SELECT lgp
- FROM LearningGoalProgress lgp
- LEFT JOIN FETCH lgp.user
- LEFT JOIN FETCH lgp.learningGoal
- WHERE lgp.learningGoal.id = :learningGoalId
- AND lgp.user.id = :userId
- """)
- Optional findEagerByLearningGoalIdAndUserId(@Param("learningGoalId") Long learningGoalId, @Param("userId") Long userId);
-
- @Query("""
- SELECT AVG(lgp.confidence)
- FROM LearningGoalProgress lgp
- WHERE lgp.learningGoal.id = :learningGoalId
- """)
- Optional findAverageConfidenceByLearningGoalId(@Param("learningGoalId") Long learningGoalId);
-
- @Query("""
- SELECT count(lgp)
- FROM LearningGoalProgress lgp
- WHERE lgp.learningGoal.id = :learningGoalId
- """)
- Long countByLearningGoal(@Param("learningGoalId") Long learningGoalId);
-
- @Query("""
- SELECT count(lgp)
- FROM LearningGoalProgress lgp
- WHERE lgp.learningGoal.id = :learningGoalId
- AND lgp.progress >= :progress
- AND lgp.confidence >= :confidence
- """)
- Long countByLearningGoalAndProgressAndConfidenceGreaterThanEqual(@Param("learningGoalId") Long learningGoalId, @Param("progress") Double progress,
- @Param("confidence") Double confidence);
-
-}
diff --git a/src/main/java/de/tum/in/www1/artemis/repository/LearningGoalRelationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/LearningGoalRelationRepository.java
deleted file mode 100644
index 18c59bc041ca..000000000000
--- a/src/main/java/de/tum/in/www1/artemis/repository/LearningGoalRelationRepository.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package de.tum.in.www1.artemis.repository;
-
-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 org.springframework.stereotype.Repository;
-
-import de.tum.in.www1.artemis.domain.LearningGoalRelation;
-
-/**
- * Spring Data JPA repository for the Learning Goal Relation entity.
- */
-@Repository
-public interface LearningGoalRelationRepository extends JpaRepository {
-
- @Query("""
- SELECT relation
- FROM LearningGoalRelation relation
- WHERE relation.headLearningGoal.id = :#{#learningGoalId}
- OR relation.tailLearningGoal.id = :#{#learningGoalId}
- """)
- Set findAllByLearningGoalId(@Param("learningGoalId") Long learningGoalId);
-
- @Query("""
- SELECT relation
- FROM LearningGoalRelation relation
- LEFT JOIN FETCH relation.headLearningGoal
- LEFT JOIN FETCH relation.tailLearningGoal
- WHERE relation.headLearningGoal.course.id = :#{#courseId}
- AND relation.tailLearningGoal.course.id = :#{#courseId}
- """)
- Set findAllByCourseId(@Param("courseId") Long courseId);
-
-}
diff --git a/src/main/java/de/tum/in/www1/artemis/repository/LearningGoalRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/LearningGoalRepository.java
deleted file mode 100644
index e5b1ba750de3..000000000000
--- a/src/main/java/de/tum/in/www1/artemis/repository/LearningGoalRepository.java
+++ /dev/null
@@ -1,183 +0,0 @@
-package de.tum.in.www1.artemis.repository;
-
-import java.util.Optional;
-import java.util.Set;
-
-import org.springframework.cache.annotation.Cacheable;
-import org.springframework.data.domain.Page;
-import org.springframework.data.domain.Pageable;
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.Query;
-import org.springframework.data.repository.query.Param;
-import org.springframework.stereotype.Repository;
-
-import de.tum.in.www1.artemis.domain.LearningGoal;
-import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException;
-
-/**
- * Spring Data JPA repository for the Learning Goal entity.
- */
-@Repository
-public interface LearningGoalRepository extends JpaRepository {
-
- @Query("""
- SELECT lg
- FROM LearningGoal lg
- LEFT JOIN FETCH lg.userProgress progress
- WHERE lg.course.id = :courseId
- """)
- Set findAllForCourse(@Param("courseId") Long courseId);
-
- @Query("""
- SELECT lg
- FROM LearningGoal lg
- LEFT JOIN FETCH lg.userProgress progress
- WHERE lg.course.id = :courseId
- AND (progress IS NULL OR progress.user.id = :userId)
- """)
- Set findAllForCourseWithProgressForUser(@Param("courseId") Long courseId, @Param("userId") Long userId);
-
- @Query("""
- SELECT lg
- FROM LearningGoal lg
- LEFT JOIN FETCH lg.exercises ex
- WHERE lg.id = :#{#learningGoalId}
- """)
- Optional findByIdWithExercises(@Param("learningGoalId") long learningGoalId);
-
- @Query("""
- SELECT lg
- FROM LearningGoal lg
- LEFT JOIN FETCH lg.lectureUnits lu
- WHERE lg.id = :#{#learningGoalId}
- """)
- Optional findByIdWithLectureUnits(@Param("learningGoalId") long learningGoalId);
-
- @Query("""
- SELECT lg
- FROM LearningGoal lg
- LEFT JOIN FETCH lg.userProgress
- LEFT JOIN FETCH lg.exercises
- LEFT JOIN FETCH lg.lectureUnits lu
- LEFT JOIN FETCH lu.completedUsers
- LEFT JOIN FETCH lu.lecture l
- LEFT JOIN FETCH lu.exercise e
- WHERE lg.id = :learningGoalId
- """)
- Optional findByIdWithExercisesAndLectureUnits(@Param("learningGoalId") Long learningGoalId);
-
- @Query("""
- SELECT lg
- FROM LearningGoal lg
- LEFT JOIN FETCH lg.lectureUnits lu
- LEFT JOIN FETCH lu.completedUsers
- WHERE lg.id = :learningGoalId
- """)
- Optional findByIdWithLectureUnitsAndCompletions(@Param("learningGoalId") Long learningGoalId);
-
- @Query("""
- SELECT lg
- FROM LearningGoal lg
- LEFT JOIN FETCH lg.exercises
- LEFT JOIN FETCH lg.lectureUnits lu
- LEFT JOIN FETCH lu.completedUsers
- WHERE lg.id = :learningGoalId
- """)
- Optional findByIdWithExercisesAndLectureUnitsAndCompletions(@Param("learningGoalId") Long learningGoalId);
-
- @Query("""
- SELECT lg
- FROM LearningGoal lg
- LEFT JOIN FETCH lg.exercises ex
- LEFT JOIN FETCH ex.learningGoals
- LEFT JOIN FETCH lg.lectureUnits lu
- LEFT JOIN FETCH lu.learningGoals
- WHERE lg.id = :learningGoalId
- """)
- Optional findByIdWithExercisesAndLectureUnitsBidirectional(@Param("learningGoalId") Long learningGoalId);
-
- @Query("""
- SELECT lg
- FROM LearningGoal lg
- LEFT JOIN FETCH lg.consecutiveCourses
- WHERE lg.id = :learningGoalId
- """)
- Optional findByIdWithConsecutiveCourses(@Param("learningGoalId") Long learningGoalId);
-
- @Query("""
- SELECT pr
- FROM LearningGoal pr
- LEFT JOIN FETCH pr.consecutiveCourses c
- WHERE c.id = :courseId
- ORDER BY pr.title
- """)
- Set findPrerequisitesByCourseId(@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.
- *
- * @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 lg
- FROM LearningGoal lg
- WHERE (lg.course.instructorGroupName IN :groups OR lg.course.editorGroupName IN :groups)
- AND (lg.title LIKE %:partialTitle% OR lg.course.title LIKE %:partialCourseTitle%)
- """)
- Page findByTitleInLectureOrCourseAndUserHasAccessToCourse(@Param("partialTitle") String partialTitle, @Param("partialCourseTitle") String partialCourseTitle,
- @Param("groups") Set groups, Pageable pageable);
-
- /**
- * Returns the title of the competency with the given id.
- *
- * @param learningGoalId the id of the competency
- * @return the name/title of the competency or null if the competency does not exist
- */
- @Query("""
- SELECT lg.title
- FROM LearningGoal lg
- WHERE lg.id = :learningGoalId
- """)
- @Cacheable(cacheNames = "learningGoalTitle", key = "#learningGoalId", unless = "#result == null")
- String getLearningGoalTitle(@Param("learningGoalId") Long learningGoalId);
-
- @SuppressWarnings("PMD.MethodNamingConventions")
- Page findByTitleIgnoreCaseContainingOrCourse_TitleIgnoreCaseContaining(String partialTitle, String partialCourseTitle, Pageable pageable);
-
- default LearningGoal findByIdWithLectureUnitsAndCompletionsElseThrow(long learningGoalId) {
- return findByIdWithLectureUnitsAndCompletions(learningGoalId).orElseThrow(() -> new EntityNotFoundException("LearningGoal", learningGoalId));
- }
-
- default LearningGoal findByIdWithExercisesAndLectureUnitsAndCompletionsElseThrow(long learningGoalId) {
- return findByIdWithExercisesAndLectureUnitsAndCompletions(learningGoalId).orElseThrow(() -> new EntityNotFoundException("LearningGoal", learningGoalId));
- }
-
- default LearningGoal findByIdWithExercisesAndLectureUnitsBidirectionalElseThrow(long learningGoalId) {
- return findByIdWithExercisesAndLectureUnitsBidirectional(learningGoalId).orElseThrow(() -> new EntityNotFoundException("LearningGoal", learningGoalId));
- }
-
- default LearningGoal findByIdWithConsecutiveCoursesElseThrow(long learningGoalId) {
- return findByIdWithConsecutiveCourses(learningGoalId).orElseThrow(() -> new EntityNotFoundException("LearningGoal", learningGoalId));
- }
-
- default LearningGoal findByIdElseThrow(Long learningGoalId) {
- return findById(learningGoalId).orElseThrow(() -> new EntityNotFoundException("LearningGoal", learningGoalId));
- }
-
- default LearningGoal findByIdWithLectureUnitsElseThrow(Long learningGoalId) {
- return findByIdWithLectureUnits(learningGoalId).orElseThrow(() -> new EntityNotFoundException("LearningGoal", learningGoalId));
- }
-
- default LearningGoal findByIdWithExercisesAndLectureUnitsElseThrow(Long learningGoalId) {
- return findByIdWithExercisesAndLectureUnits(learningGoalId).orElseThrow(() -> new EntityNotFoundException("LearningGoal", learningGoalId));
- }
-
- default LearningGoal findByIdWithExercisesElseThrow(Long learningGoalId) {
- return findByIdWithExercises(learningGoalId).orElseThrow(() -> new EntityNotFoundException("LearningGoal", learningGoalId));
- }
-}
diff --git a/src/main/java/de/tum/in/www1/artemis/repository/LectureRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/LectureRepository.java
index ca272afb28c6..91beb1ec3341 100644
--- a/src/main/java/de/tum/in/www1/artemis/repository/LectureRepository.java
+++ b/src/main/java/de/tum/in/www1/artemis/repository/LectureRepository.java
@@ -57,23 +57,23 @@ public interface LectureRepository extends JpaRepository {
LEFT JOIN FETCH lecture.posts
LEFT JOIN FETCH lecture.lectureUnits lu
LEFT JOIN FETCH lu.completedUsers cu
- LEFT JOIN FETCH lu.learningGoals
+ LEFT JOIN FETCH lu.competencies
LEFT JOIN FETCH lu.exercise exercise
- LEFT JOIN FETCH exercise.learningGoals
+ LEFT JOIN FETCH exercise.competencies
WHERE lecture.id = :#{#lectureId}
""")
- Optional findByIdWithPostsAndLectureUnitsAndLearningGoalsAndCompletions(@Param("lectureId") Long lectureId);
+ Optional findByIdWithPostsAndLectureUnitsAndCompetenciesAndCompletions(@Param("lectureId") Long lectureId);
@Query("""
SELECT lecture
FROM Lecture lecture
LEFT JOIN FETCH lecture.lectureUnits lu
- LEFT JOIN FETCH lu.learningGoals
+ LEFT JOIN FETCH lu.competencies
LEFT JOIN FETCH lu.exercise exercise
- LEFT JOIN FETCH exercise.learningGoals
+ LEFT JOIN FETCH exercise.competencies
WHERE lecture.id = :#{#lectureId}
""")
- Optional findByIdWithLectureUnitsAndLearningGoals(@Param("lectureId") Long lectureId);
+ Optional findByIdWithLectureUnitsAndCompetencies(@Param("lectureId") Long lectureId);
@Query("""
SELECT lecture
@@ -134,13 +134,13 @@ default Lecture findByIdElseThrow(long lectureId) {
}
@NotNull
- default Lecture findByIdWithLectureUnitsAndLearningGoalsElseThrow(Long lectureId) {
- return findByIdWithLectureUnitsAndLearningGoals(lectureId).orElseThrow(() -> new EntityNotFoundException("Lecture", lectureId));
+ default Lecture findByIdWithLectureUnitsAndCompetenciesElseThrow(Long lectureId) {
+ return findByIdWithLectureUnitsAndCompetencies(lectureId).orElseThrow(() -> new EntityNotFoundException("Lecture", lectureId));
}
@NotNull
- default Lecture findByIdWithPostsAndLectureUnitsAndLearningGoalsAndCompletionsElseThrow(Long lectureId) {
- return findByIdWithPostsAndLectureUnitsAndLearningGoalsAndCompletions(lectureId).orElseThrow(() -> new EntityNotFoundException("Lecture", lectureId));
+ default Lecture findByIdWithPostsAndLectureUnitsAndCompetenciesAndCompletionsElseThrow(Long lectureId) {
+ return findByIdWithPostsAndLectureUnitsAndCompetenciesAndCompletions(lectureId).orElseThrow(() -> new EntityNotFoundException("Lecture", lectureId));
}
@NotNull
diff --git a/src/main/java/de/tum/in/www1/artemis/repository/LectureUnitRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/LectureUnitRepository.java
index 5ecd8891f951..37142b28d9ee 100644
--- a/src/main/java/de/tum/in/www1/artemis/repository/LectureUnitRepository.java
+++ b/src/main/java/de/tum/in/www1/artemis/repository/LectureUnitRepository.java
@@ -20,40 +20,40 @@ public interface LectureUnitRepository extends JpaRepository
@Query("""
SELECT lu
FROM LectureUnit lu
- LEFT JOIN FETCH lu.learningGoals
+ LEFT JOIN FETCH lu.competencies
LEFT JOIN FETCH lu.exercise exercise
- LEFT JOIN FETCH exercise.learningGoals
+ LEFT JOIN FETCH exercise.competencies
WHERE lu.id = :lectureUnitId
""")
- Optional findByIdWithLearningGoals(@Param("lectureUnitId") Long lectureUnitId);
+ Optional findByIdWithCompetencies(@Param("lectureUnitId") Long lectureUnitId);
@Query("""
SELECT lu
FROM LectureUnit lu
- LEFT JOIN FETCH lu.learningGoals lg
- LEFT JOIN FETCH lg.lectureUnits
+ LEFT JOIN FETCH lu.competencies c
+ LEFT JOIN FETCH c.lectureUnits
LEFT JOIN FETCH lu.exercise ex
- LEFT JOIN FETCH ex.learningGoals
+ LEFT JOIN FETCH ex.competencies
WHERE lu.id = :lectureUnitId
""")
- Optional findByIdWithLearningGoalsBidirectional(@Param("lectureUnitId") long lectureUnitId);
+ Optional findByIdWithCompetenciesBidirectional(@Param("lectureUnitId") long lectureUnitId);
@Query("""
SELECT lu
FROM LectureUnit lu
- LEFT JOIN FETCH lu.learningGoals lg
- LEFT JOIN FETCH lg.lectureUnits
+ LEFT JOIN FETCH lu.competencies c
+ LEFT JOIN FETCH c.lectureUnits
LEFT JOIN FETCH lu.exercise ex
- LEFT JOIN FETCH ex.learningGoals
+ LEFT JOIN FETCH ex.competencies
WHERE lu.id IN :lectureUnitIds
""")
- Set findAllByIdWithLearningGoalsBidirectional(@Param("lectureUnitIds") Iterable longs);
+ Set findAllByIdWithCompetenciesBidirectional(@Param("lectureUnitIds") Iterable longs);
- default LectureUnit findByIdWithLearningGoalsBidirectionalElseThrow(long lectureUnitId) {
- return findByIdWithLearningGoalsBidirectional(lectureUnitId).orElseThrow(() -> new EntityNotFoundException("LectureUnit", lectureUnitId));
+ default LectureUnit findByIdWithCompetenciesBidirectionalElseThrow(long lectureUnitId) {
+ return findByIdWithCompetenciesBidirectional(lectureUnitId).orElseThrow(() -> new EntityNotFoundException("LectureUnit", lectureUnitId));
}
- default LectureUnit findByIdWithLearningGoalsElseThrow(long lectureUnitId) {
- return findByIdWithLearningGoals(lectureUnitId).orElseThrow(() -> new EntityNotFoundException("LectureUnit", lectureUnitId));
+ default LectureUnit findByIdWithCompetenciesElseThrow(long lectureUnitId) {
+ return findByIdWithCompetencies(lectureUnitId).orElseThrow(() -> new EntityNotFoundException("LectureUnit", lectureUnitId));
}
}
diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ModelingExerciseRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ModelingExerciseRepository.java
index b6b28209369f..92e6c45523ca 100644
--- a/src/main/java/de/tum/in/www1/artemis/repository/ModelingExerciseRepository.java
+++ b/src/main/java/de/tum/in/www1/artemis/repository/ModelingExerciseRepository.java
@@ -32,8 +32,8 @@ public interface ModelingExerciseRepository extends JpaRepository findByCourseIdWithCategories(@Param("courseId") Long courseId);
- @EntityGraph(type = LOAD, attributePaths = { "exampleSubmissions", "teamAssignmentConfig", "categories", "learningGoals", "exampleSubmissions.submission.results" })
- Optional findWithEagerExampleSubmissionsAndLearningGoalsById(@Param("exerciseId") Long exerciseId);
+ @EntityGraph(type = LOAD, attributePaths = { "exampleSubmissions", "teamAssignmentConfig", "categories", "competencies", "exampleSubmissions.submission.results" })
+ Optional findWithEagerExampleSubmissionsAndCompetenciesById(@Param("exerciseId") Long exerciseId);
@Query("select modelingExercise from ModelingExercise modelingExercise left join fetch modelingExercise.exampleSubmissions exampleSubmissions left join fetch exampleSubmissions.submission submission left join fetch submission.results results left join fetch results.feedbacks left join fetch results.assessor left join fetch modelingExercise.teamAssignmentConfig where modelingExercise.id = :#{#exerciseId}")
Optional findByIdWithExampleSubmissionsAndResults(@Param("exerciseId") Long exerciseId);
@@ -72,8 +72,8 @@ default ModelingExercise findByIdElseThrow(long exerciseId) {
}
@NotNull
- default ModelingExercise findWithEagerExampleSubmissionsAndLearningGoalsByIdElseThrow(long exerciseId) {
- return findWithEagerExampleSubmissionsAndLearningGoalsById(exerciseId).orElseThrow(() -> new EntityNotFoundException("Modeling Exercise", exerciseId));
+ default ModelingExercise findWithEagerExampleSubmissionsAndCompetenciesByIdElseThrow(long exerciseId) {
+ return findWithEagerExampleSubmissionsAndCompetenciesById(exerciseId).orElseThrow(() -> new EntityNotFoundException("Modeling Exercise", exerciseId));
}
@NotNull
diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseRepository.java
index 1c125bdc6af9..b2b402d9e88e 100644
--- a/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseRepository.java
+++ b/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseRepository.java
@@ -57,9 +57,9 @@ public interface ProgrammingExerciseRepository extends JpaRepository findWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesById(Long exerciseId);
- @EntityGraph(type = LOAD, attributePaths = { "templateParticipation", "solutionParticipation", "teamAssignmentConfig", "categories", "learningGoals", "auxiliaryRepositories",
+ @EntityGraph(type = LOAD, attributePaths = { "templateParticipation", "solutionParticipation", "teamAssignmentConfig", "categories", "competencies", "auxiliaryRepositories",
"submissionPolicy" })
- Optional findWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesAndLearningGoalsById(Long exerciseId);
+ Optional findWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesAndCompetenciesById(Long exerciseId);
@EntityGraph(type = LOAD, attributePaths = { "templateParticipation", "solutionParticipation", "auxiliaryRepositories" })
Optional findWithTemplateAndSolutionParticipationAndAuxiliaryRepositoriesById(Long exerciseId);
@@ -80,6 +80,33 @@ public interface ProgrammingExerciseRepository extends JpaRepository findWithSubmissionPolicyById(Long exerciseId);
+ List findAllByProjectKey(String projectKey);
+
+ @EntityGraph(type = LOAD, attributePaths = "submissionPolicy")
+ List findWithSubmissionPolicyByProjectKey(String projectKey);
+
+ /**
+ * Finds one programming exercise including its submission policy by the exercise's project key.
+ *
+ * @param projectKey the project key of the programming exercise.
+ * @param withSubmissionPolicy whether the submission policy should be included in the result.
+ * @return the programming exercise.
+ * @throws EntityNotFoundException if no programming exercise or multiple exercises with the given project key exist.
+ */
+ default ProgrammingExercise findOneByProjectKeyOrThrow(String projectKey, boolean withSubmissionPolicy) throws EntityNotFoundException {
+ List exercises;
+ if (withSubmissionPolicy) {
+ exercises = findWithSubmissionPolicyByProjectKey(projectKey);
+ }
+ else {
+ exercises = findAllByProjectKey(projectKey);
+ }
+ if (exercises.size() != 1) {
+ throw new EntityNotFoundException("No exercise or multiple exercises found for the given project key: " + projectKey);
+ }
+ return exercises.get(0);
+ }
+
/**
* Returns all programming exercises with its test cases
*
@@ -549,9 +576,9 @@ default ProgrammingExercise findByIdWithTemplateAndSolutionParticipationTeamAssi
}
@NotNull
- default ProgrammingExercise findByIdWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesAndLearningGoalsElseThrow(long programmingExerciseId)
+ default ProgrammingExercise findByIdWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesAndCompetenciesElseThrow(long programmingExerciseId)
throws EntityNotFoundException {
- Optional programmingExercise = findWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesAndLearningGoalsById(programmingExerciseId);
+ Optional programmingExercise = findWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesAndCompetenciesById(programmingExerciseId);
return programmingExercise.orElseThrow(() -> new EntityNotFoundException("Programming Exercise", programmingExerciseId));
}
diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java
index 32f711efc81f..4a4318b12e63 100644
--- a/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java
+++ b/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java
@@ -26,17 +26,20 @@
public interface ProgrammingExerciseStudentParticipationRepository extends JpaRepository {
@Query("""
- select p from ProgrammingExerciseStudentParticipation p
- left join fetch p.results pr
- left join fetch pr.feedbacks
- left join fetch pr.submission
- where p.id = :participationId
- and (pr.id = (select max(prr.id) from p.results prr
- where (prr.assessmentType = 'AUTOMATIC'
- or (prr.completionDate IS NOT NULL
- and (p.exercise.assessmentDueDate IS NULL
+ SELECT p
+ FROM ProgrammingExerciseStudentParticipation p
+ LEFT JOIN FETCH p.results pr
+ LEFT JOIN FETCH pr.feedbacks
+ LEFT JOIN FETCH pr.submission
+ WHERE p.id = :participationId
+ AND (pr.id = (
+ SELECT max(prr.id)
+ FROM p.results prr
+ WHERE (prr.assessmentType = 'AUTOMATIC'
+ OR (prr.completionDate IS NOT NULL
+ AND (p.exercise.assessmentDueDate IS NULL
OR p.exercise.assessmentDueDate < :#{#dateTime}))))
- or pr.id IS NULL)
+ OR pr.id IS NULL)
""")
Optional findByIdWithLatestResultAndFeedbacksAndRelatedSubmissions(@Param("participationId") Long participationId,
@Param("dateTime") ZonedDateTime dateTime);
@@ -48,14 +51,18 @@ Optional findByIdWithLatestResultAndFee
* @return a participation with all its manual results.
*/
@Query("""
- select p from ProgrammingExerciseStudentParticipation p
- left join fetch p.results pr
- left join fetch pr.feedbacks
- left join fetch pr.submission
- left join fetch pr.assessor
- where p.id = :participationId
- and pr.id in (select prr.id from p.results prr
- where prr.assessmentType = 'MANUAL' or prr.assessmentType = 'SEMI_AUTOMATIC')
+ SELECT p
+ FROM ProgrammingExerciseStudentParticipation p
+ LEFT JOIN FETCH p.results pr
+ LEFT JOIN FETCH pr.feedbacks
+ LEFT JOIN FETCH pr.submission
+ LEFT JOIN FETCH pr.assessor
+ WHERE p.id = :participationId
+ AND pr.id IN (
+ SELECT prr.id
+ FROM p.results prr
+ WHERE prr.assessmentType = 'MANUAL'
+ OR prr.assessmentType = 'SEMI_AUTOMATIC')
""")
Optional findByIdWithAllManualOrSemiAutomaticResultsAndFeedbacksAndRelatedSubmissionAndAssessor(
@Param("participationId") Long participationId);
@@ -63,13 +70,57 @@ Optional findByIdWithAllManualOrSemiAut
@EntityGraph(type = LOAD, attributePaths = { "results", "exercise" })
List findByBuildPlanId(String buildPlanId);
- @Query("select distinct p from ProgrammingExerciseStudentParticipation p left join fetch p.results where p.buildPlanId is not null and (p.student is not null or p.team is not null)")
+ @Query("""
+ SELECT DISTINCT p
+ FROM ProgrammingExerciseStudentParticipation p
+ LEFT JOIN FETCH p.results
+ WHERE p.buildPlanId IS NOT NULL
+ AND (p.student IS NOT NULL
+ OR p.team IS NOT NULL)
+ """)
List findAllWithBuildPlanIdWithResults();
Optional findByExerciseIdAndStudentLogin(Long exerciseId, String username);
+ default ProgrammingExerciseStudentParticipation findByExerciseIdAndStudentLoginOrThrow(Long exerciseId, String username) {
+ return findByExerciseIdAndStudentLogin(exerciseId, username).orElseThrow(() -> new EntityNotFoundException("Programming Exercise Student Participation", exerciseId));
+ }
+
+ @EntityGraph(type = LOAD, attributePaths = { "submissions" })
+ Optional findWithSubmissionsByExerciseIdAndStudentLogin(Long exerciseId, String username);
+
+ default ProgrammingExerciseStudentParticipation findWithSubmissionsByExerciseIdAndStudentLoginOrThrow(Long exerciseId, String username) {
+ return findWithSubmissionsByExerciseIdAndStudentLogin(exerciseId, username)
+ .orElseThrow(() -> new EntityNotFoundException("Programming Exercise Student Participation", exerciseId));
+ }
+
+ Optional findByExerciseIdAndStudentLoginAndTestRun(Long exerciseId, String username, boolean testRun);
+
Optional findByExerciseIdAndTeamId(Long exerciseId, Long teamId);
+ @Query("""
+ SELECT DISTINCT participation
+ FROM ProgrammingExerciseStudentParticipation participation
+ LEFT JOIN FETCH participation.team team
+ LEFT JOIN FETCH team.students
+ WHERE participation.exercise.id = :#{#exerciseId}
+ AND participation.team.shortName = :#{#teamShortName}
+ """)
+ Optional findWithEagerStudentsByExerciseIdAndTeamShortName(@Param("exerciseId") Long exerciseId,
+ @Param("teamShortName") String teamShortName);
+
+ @Query("""
+ SELECT DISTINCT participation
+ FROM ProgrammingExerciseStudentParticipation participation
+ LEFT JOIN FETCH participation.submissions
+ LEFT JOIN FETCH participation.team team
+ LEFT JOIN FETCH team.students
+ WHERE participation.exercise.id = :#{#exerciseId}
+ AND participation.team.shortName = :#{#teamShortName}
+ """)
+ Optional findWithSubmissionsAndEagerStudentsByExerciseIdAndTeamShortName(@Param("exerciseId") Long exerciseId,
+ @Param("teamShortName") String teamShortName);
+
List findByExerciseId(Long exerciseId);
@EntityGraph(type = LOAD, attributePaths = { "submissions" })
@@ -83,19 +134,31 @@ Optional findByIdWithAllManualOrSemiAut
* @return filtered list of participations.
*/
@Query("""
- select participation from ProgrammingExerciseStudentParticipation participation
- left join fetch participation.submissions
- where participation.exercise.id = :#{#exerciseId}
- and participation.id in :#{#participationIds}
+ SELECT participation
+ FROM ProgrammingExerciseStudentParticipation participation
+ LEFT JOIN FETCH participation.submissions
+ WHERE participation.exercise.id = :#{#exerciseId}
+ AND participation.id IN :#{#participationIds}
""")
List findWithSubmissionsByExerciseIdAndParticipationIds(@Param("exerciseId") Long exerciseId,
@Param("participationIds") Collection participationIds);
+ @Query("""
+ SELECT participation
+ FROM ProgrammingExerciseStudentParticipation participation
+ LEFT JOIN FETCH participation.submissions
+ WHERE participation.exercise.id = :#{#exerciseId}
+ AND participation.student.login = :#{#username}
+ AND participation.testRun = :#{#testRun}
+ """)
+ Optional findWithSubmissionsByExerciseIdAndStudentLoginAndTestRun(@Param("exerciseId") Long exerciseId,
+ @Param("username") String username, @Param("testRun") boolean testRun);
+
@Query("""
SELECT p
FROM ProgrammingExerciseStudentParticipation p
WHERE p.exercise.id = :#{#exerciseId}
- AND p.individualDueDate IS NOT null
+ AND p.individualDueDate IS NOT NULL
""")
List findWithIndividualDueDateByExerciseId(@Param("exerciseId") Long exerciseId);
diff --git a/src/main/java/de/tum/in/www1/artemis/repository/QuizExerciseRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/QuizExerciseRepository.java
index 5af633bc788e..6b8ce52351d3 100644
--- a/src/main/java/de/tum/in/www1/artemis/repository/QuizExerciseRepository.java
+++ b/src/main/java/de/tum/in/www1/artemis/repository/QuizExerciseRepository.java
@@ -51,8 +51,8 @@ public interface QuizExerciseRepository extends JpaRepository findWithEagerQuestionsAndStatisticsById(Long quizExerciseId);
- @EntityGraph(type = LOAD, attributePaths = { "quizQuestions", "quizPointStatistic", "quizQuestions.quizQuestionStatistic", "categories", "learningGoals", "quizBatches" })
- Optional findWithEagerQuestionsAndStatisticsAndLearningGoalsById(Long quizExerciseId);
+ @EntityGraph(type = LOAD, attributePaths = { "quizQuestions", "quizPointStatistic", "quizQuestions.quizQuestionStatistic", "categories", "competencies", "quizBatches" })
+ Optional findWithEagerQuestionsAndStatisticsAndCompetenciesById(Long quizExerciseId);
@EntityGraph(type = LOAD, attributePaths = { "quizQuestions" })
Optional findWithEagerQuestionsById(Long quizExerciseId);
@@ -115,8 +115,8 @@ default QuizExercise findByIdWithQuestionsAndStatisticsElseThrow(Long quizExerci
}
@NotNull
- default QuizExercise findByIdWithQuestionsAndStatisticsAndLearningGoalsElseThrow(Long quizExerciseId) {
- return findWithEagerQuestionsAndStatisticsAndLearningGoalsById(quizExerciseId).orElseThrow(() -> new EntityNotFoundException("Quiz Exercise", quizExerciseId));
+ default QuizExercise findByIdWithQuestionsAndStatisticsAndCompetenciesElseThrow(Long quizExerciseId) {
+ return findWithEagerQuestionsAndStatisticsAndCompetenciesById(quizExerciseId).orElseThrow(() -> new EntityNotFoundException("Quiz Exercise", quizExerciseId));
}
default List findAllPlannedToStartInTheFuture() {
diff --git a/src/main/java/de/tum/in/www1/artemis/repository/SolutionProgrammingExerciseParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/SolutionProgrammingExerciseParticipationRepository.java
index a154a4adc979..e673494f0649 100644
--- a/src/main/java/de/tum/in/www1/artemis/repository/SolutionProgrammingExerciseParticipationRepository.java
+++ b/src/main/java/de/tum/in/www1/artemis/repository/SolutionProgrammingExerciseParticipationRepository.java
@@ -27,6 +27,10 @@ public interface SolutionProgrammingExerciseParticipationRepository extends JpaR
@EntityGraph(type = LOAD, attributePaths = { "results", "submissions", "submissions.results" })
Optional findWithEagerResultsAndSubmissionsByProgrammingExerciseId(Long exerciseId);
+ default SolutionProgrammingExerciseParticipation findWithEagerResultsAndSubmissionsByProgrammingExerciseIdElseThrow(Long exerciseId) {
+ return findWithEagerResultsAndSubmissionsByProgrammingExerciseId(exerciseId).orElseThrow(() -> new EntityNotFoundException("ProgrammingExerciseParticipation", exerciseId));
+ }
+
@EntityGraph(type = LOAD, attributePaths = { "results", "results.feedbacks", "submissions" })
Optional findWithEagerResultsAndFeedbacksAndSubmissionsByProgrammingExerciseId(Long exerciseId);
diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java
index 5529e79eac7f..55217cdf2d19 100644
--- a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java
+++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java
@@ -84,6 +84,8 @@ SELECT COUNT(p.id) > 0
List findByTeamId(Long teamId);
+ Optional findByExerciseIdAndStudentLoginAndTestRun(Long exerciseId, String username, boolean testRun);
+
@EntityGraph(type = LOAD, attributePaths = "results")
Optional findWithEagerResultsByExerciseIdAndStudentLoginAndTestRun(Long exerciseId, String username, boolean testRun);
@@ -117,6 +119,16 @@ SELECT COUNT(p.id) > 0
Optional findWithEagerLegalSubmissionsByExerciseIdAndStudentLoginAndTestRun(@Param("exerciseId") Long exerciseId, @Param("username") String username,
@Param("testRun") boolean testRun);
+ @Query("""
+ SELECT p FROM StudentParticipation p
+ LEFT JOIN FETCH p.submissions
+ WHERE p.exercise.id = :#{#exerciseId}
+ AND p.student.login = :#{#username}
+ AND p.testRun = :#{#testRun}
+ """)
+ Optional findWithEagerSubmissionsByExerciseIdAndStudentLoginAndTestRun(@Param("exerciseId") Long exerciseId, @Param("username") String username,
+ @Param("testRun") boolean testRun);
+
@Query("""
SELECT DISTINCT p FROM StudentParticipation p
where p.exercise.id = :#{#exerciseId}
diff --git a/src/main/java/de/tum/in/www1/artemis/repository/TemplateProgrammingExerciseParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/TemplateProgrammingExerciseParticipationRepository.java
index 8f697e82e7b6..3a1a2abce1f4 100644
--- a/src/main/java/de/tum/in/www1/artemis/repository/TemplateProgrammingExerciseParticipationRepository.java
+++ b/src/main/java/de/tum/in/www1/artemis/repository/TemplateProgrammingExerciseParticipationRepository.java
@@ -27,6 +27,10 @@ public interface TemplateProgrammingExerciseParticipationRepository extends JpaR
@EntityGraph(type = LOAD, attributePaths = { "results", "submissions" })
Optional findWithEagerResultsAndSubmissionsByProgrammingExerciseId(Long exerciseId);
+ default TemplateProgrammingExerciseParticipation findWithEagerResultsAndSubmissionsByProgrammingExerciseIdElseThrow(Long exerciseId) {
+ return findWithEagerResultsAndSubmissionsByProgrammingExerciseId(exerciseId).orElseThrow(() -> new EntityNotFoundException("ProgrammingExerciseParticipation", exerciseId));
+ }
+
@EntityGraph(type = LOAD, attributePaths = { "results", "results.feedbacks", "submissions" })
Optional findWithEagerResultsAndFeedbacksAndSubmissionsByProgrammingExerciseId(Long exerciseId);
diff --git a/src/main/java/de/tum/in/www1/artemis/repository/TextExerciseRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/TextExerciseRepository.java
index 9207d3b5fdd2..4346f75591fe 100644
--- a/src/main/java/de/tum/in/www1/artemis/repository/TextExerciseRepository.java
+++ b/src/main/java/de/tum/in/www1/artemis/repository/TextExerciseRepository.java
@@ -33,8 +33,8 @@ public interface TextExerciseRepository extends JpaRepository findByCourseIdWithCategories(@Param("courseId") Long courseId);
- @EntityGraph(type = LOAD, attributePaths = { "teamAssignmentConfig", "categories", "learningGoals" })
- Optional findWithEagerTeamAssignmentConfigAndCategoriesAndLearningGoalsById(Long exerciseId);
+ @EntityGraph(type = LOAD, attributePaths = { "teamAssignmentConfig", "categories", "competencies" })
+ Optional findWithEagerTeamAssignmentConfigAndCategoriesAndCompetenciesById(Long exerciseId);
List findByAssessmentTypeAndDueDateIsAfter(AssessmentType assessmentType, ZonedDateTime dueDate);
diff --git a/src/main/java/de/tum/in/www1/artemis/repository/TextUnitRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/TextUnitRepository.java
index 689ece0d5df5..1407ab1891ea 100644
--- a/src/main/java/de/tum/in/www1/artemis/repository/TextUnitRepository.java
+++ b/src/main/java/de/tum/in/www1/artemis/repository/TextUnitRepository.java
@@ -17,17 +17,17 @@ public interface TextUnitRepository extends JpaRepository {
@Query("""
SELECT tu FROM TextUnit tu
- LEFT JOIN FETCH tu.learningGoals
+ LEFT JOIN FETCH tu.competencies
WHERE tu.id = :textUnitId
""")
- Optional findByIdWithLearningGoals(@Param("textUnitId") Long textUnitId);
+ Optional findByIdWithCompetencies(@Param("textUnitId") Long textUnitId);
@Query("""
SELECT tu FROM TextUnit tu
- LEFT JOIN FETCH tu.learningGoals lg
- LEFT JOIN FETCH lg.lectureUnits
+ LEFT JOIN FETCH tu.competencies c
+ LEFT JOIN FETCH c.lectureUnits
WHERE tu.id = :textUnitId
""")
- Optional findByIdWithLearningGoalsBidirectional(@Param("textUnitId") Long textUnitId);
+ Optional findByIdWithCompetenciesBidirectional(@Param("textUnitId") Long textUnitId);
}
diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningGoalProgressService.java b/src/main/java/de/tum/in/www1/artemis/service/CompetencyProgressService.java
similarity index 74%
rename from src/main/java/de/tum/in/www1/artemis/service/LearningGoalProgressService.java
rename to src/main/java/de/tum/in/www1/artemis/service/CompetencyProgressService.java
index 449512a79abf..ff6b3f5ad841 100644
--- a/src/main/java/de/tum/in/www1/artemis/service/LearningGoalProgressService.java
+++ b/src/main/java/de/tum/in/www1/artemis/service/CompetencyProgressService.java
@@ -26,13 +26,13 @@
* Service for calculating the progress of a student in a competency.
*/
@Service
-public class LearningGoalProgressService {
+public class CompetencyProgressService {
- private final Logger logger = LoggerFactory.getLogger(LearningGoalProgressService.class);
+ private final Logger logger = LoggerFactory.getLogger(CompetencyProgressService.class);
- private final LearningGoalRepository learningGoalRepository;
+ private final CompetencyRepository competencyRepository;
- private final LearningGoalProgressRepository learningGoalProgressRepository;
+ private final CompetencyProgressRepository competencyProgressRepository;
private final StudentScoreRepository studentScoreRepository;
@@ -44,11 +44,11 @@ public class LearningGoalProgressService {
private final UserRepository userRepository;
- public LearningGoalProgressService(LearningGoalRepository learningGoalRepository, LearningGoalProgressRepository learningGoalProgressRepository,
+ public CompetencyProgressService(CompetencyRepository competencyRepository, CompetencyProgressRepository competencyProgressRepository,
StudentScoreRepository studentScoreRepository, TeamScoreRepository teamScoreRepository, ExerciseRepository exerciseRepository,
LectureUnitRepository lectureUnitRepository, UserRepository userRepository) {
- this.learningGoalRepository = learningGoalRepository;
- this.learningGoalProgressRepository = learningGoalProgressRepository;
+ this.competencyRepository = competencyRepository;
+ this.competencyProgressRepository = competencyProgressRepository;
this.studentScoreRepository = studentScoreRepository;
this.teamScoreRepository = teamScoreRepository;
this.exerciseRepository = exerciseRepository;
@@ -92,13 +92,13 @@ else if (learningObject instanceof LectureUnit lectureUnit) {
/**
* Asynchronously update the existing progress for a specific competency
*
- * @param learningGoal The competency for which to update all existing student progress
+ * @param competency The competency for which to update all existing student progress
*/
@Async
- public void updateProgressByLearningGoalAsync(LearningGoal learningGoal) {
+ public void updateProgressByCompetencyAsync(Competency competency) {
SecurityUtils.setAuthorizationObject(); // required for async
- learningGoalProgressRepository.findAllByLearningGoalId(learningGoal.getId()).stream().map(LearningGoalProgress::getUser).forEach(user -> {
- updateLearningGoalProgress(learningGoal.getId(), user);
+ competencyProgressRepository.findAllByCompetencyId(competency.getId()).stream().map(CompetencyProgress::getUser).forEach(user -> {
+ updateCompetencyProgress(competency.getId(), user);
});
}
@@ -109,15 +109,15 @@ public void updateProgressByLearningGoalAsync(LearningGoal learningGoal) {
* @param course The course for which to fetch the competencies from
* @return All competencies of the course with the updated progress for the user
*/
- public Set getLearningGoalsAndUpdateProgressByUserInCourse(User user, Course course) {
- var learningGoals = learningGoalRepository.findAllForCourse(course.getId());
- learningGoals.forEach(learningGoal -> {
- var updatedProgress = updateLearningGoalProgress(learningGoal.getId(), user);
+ public Set getCompetenciesAndUpdateProgressByUserInCourse(User user, Course course) {
+ var competencies = competencyRepository.findAllForCourse(course.getId());
+ competencies.forEach(competency -> {
+ var updatedProgress = updateCompetencyProgress(competency.getId(), user);
if (updatedProgress != null) {
- learningGoal.setUserProgress(Set.of(updatedProgress));
+ competency.setUserProgress(Set.of(updatedProgress));
}
});
- return learningGoals;
+ return competencies;
}
/**
@@ -129,26 +129,26 @@ public Set getLearningGoalsAndUpdateProgressByUserInCourse(User us
public void updateProgressByLearningObject(LearningObject learningObject, @NotNull Set users) {
logger.debug("Updating competency progress for {} users.", users.size());
try {
- Set learningGoals;
+ Set competencies;
if (learningObject instanceof Exercise exercise) {
- learningGoals = exerciseRepository.findByIdWithLearningGoals(exercise.getId()).map(Exercise::getLearningGoals).orElse(null);
+ competencies = exerciseRepository.findByIdWithCompetencies(exercise.getId()).map(Exercise::getCompetencies).orElse(null);
}
else if (learningObject instanceof LectureUnit lectureUnit) {
- learningGoals = lectureUnitRepository.findByIdWithLearningGoals(lectureUnit.getId()).map(LectureUnit::getLearningGoals).orElse(null);
+ competencies = lectureUnitRepository.findByIdWithCompetencies(lectureUnit.getId()).map(LectureUnit::getCompetencies).orElse(null);
}
else {
throw new IllegalArgumentException("Learning object must be either LectureUnit or Exercise");
}
- if (learningGoals == null) {
+ if (competencies == null) {
// Competencies couldn't be loaded, the exercise/lecture unit might have already been deleted
logger.debug("Competencies could not be fetched, skipping.");
return;
}
users.forEach(user -> {
- learningGoals.forEach(learningGoal -> {
- updateLearningGoalProgress(learningGoal.getId(), user);
+ competencies.forEach(competency -> {
+ updateCompetencyProgress(competency.getId(), user);
});
});
}
@@ -160,61 +160,61 @@ else if (learningObject instanceof LectureUnit lectureUnit) {
/**
* Updates the progress value (and confidence score) of the given competency and user, then returns it
*
- * @param learningGoalId The id of the competency to update the progress for
- * @param user The user for which the progress should be updated
+ * @param competencyId The id of the competency to update the progress for
+ * @param user The user for which the progress should be updated
* @return The updated competency progress, which is also persisted to the database
*/
- public LearningGoalProgress updateLearningGoalProgress(Long learningGoalId, User user) {
- var learningGoal = learningGoalRepository.findByIdWithExercisesAndLectureUnitsAndCompletions(learningGoalId).orElse(null);
+ public CompetencyProgress updateCompetencyProgress(Long competencyId, User user) {
+ var competency = competencyRepository.findByIdWithExercisesAndLectureUnitsAndCompletions(competencyId).orElse(null);
- if (user == null || learningGoal == null) {
+ if (user == null || competency == null) {
logger.debug("User or competency no longer exist, skipping.");
return null;
}
- var learningGoalProgress = learningGoalProgressRepository.findEagerByLearningGoalIdAndUserId(learningGoalId, user.getId());
+ var competencyProgress = competencyProgressRepository.findEagerByCompetencyIdAndUserId(competencyId, user.getId());
- if (learningGoalProgress.isPresent()) {
- var lastModified = learningGoalProgress.get().getLastModifiedDate();
+ if (competencyProgress.isPresent()) {
+ var lastModified = competencyProgress.get().getLastModifiedDate();
if (lastModified != null && lastModified.isAfter(Instant.now().minusSeconds(1))) {
logger.debug("Competency progress has been updated very recently, skipping.");
- return learningGoalProgress.get();
+ return competencyProgress.get();
}
}
- var studentProgress = learningGoalProgress.orElse(new LearningGoalProgress());
+ var studentProgress = competencyProgress.orElse(new CompetencyProgress());
List learningObjects = new ArrayList<>();
- List allLectureUnits = learningGoal.getLectureUnits().stream().filter(LectureUnit::isVisibleToStudents).toList();
+ List allLectureUnits = competency.getLectureUnits().stream().filter(LectureUnit::isVisibleToStudents).toList();
List lectureUnits = allLectureUnits.stream().filter(lectureUnit -> !(lectureUnit instanceof ExerciseUnit)).toList();
- List exercises = learningGoal.getExercises().stream().filter(Exercise::isVisibleToStudents).toList();
+ List exercises = competency.getExercises().stream().filter(Exercise::isVisibleToStudents).toList();
learningObjects.addAll(lectureUnits);
learningObjects.addAll(exercises);
- var progress = RoundingUtil.roundScoreSpecifiedByCourseSettings(calculateProgress(learningObjects, user), learningGoal.getCourse());
- var confidence = RoundingUtil.roundScoreSpecifiedByCourseSettings(calculateConfidence(exercises, user), learningGoal.getCourse());
+ var progress = RoundingUtil.roundScoreSpecifiedByCourseSettings(calculateProgress(learningObjects, user), competency.getCourse());
+ var confidence = RoundingUtil.roundScoreSpecifiedByCourseSettings(calculateConfidence(exercises, user), competency.getCourse());
if (exercises.isEmpty()) {
// If the competency has no exercises, the confidence score equals the progress
confidence = progress;
}
- studentProgress.setLearningGoal(learningGoal);
+ studentProgress.setCompetency(competency);
studentProgress.setUser(user);
studentProgress.setProgress(progress);
studentProgress.setConfidence(confidence);
try {
- learningGoalProgressRepository.save(studentProgress);
+ competencyProgressRepository.save(studentProgress);
}
catch (DataIntegrityViolationException e) {
// In rare instances of initially creating a progress entity, async updates might run in parallel.
// This fails the SQL unique constraint and throws an exception. We can safely ignore it.
}
- logger.debug("Updated progress for user {} in competency {} to {} / {}.", user.getLogin(), learningGoal.getId(), studentProgress.getProgress(),
+ logger.debug("Updated progress for user {} in competency {} to {} / {}.", user.getLogin(), competency.getId(), studentProgress.getProgress(),
studentProgress.getConfidence());
return studentProgress;
}
diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningGoalService.java b/src/main/java/de/tum/in/www1/artemis/service/CompetencyService.java
similarity index 72%
rename from src/main/java/de/tum/in/www1/artemis/service/LearningGoalService.java
rename to src/main/java/de/tum/in/www1/artemis/service/CompetencyService.java
index 2df6974a1fc4..0a36f15495fd 100644
--- a/src/main/java/de/tum/in/www1/artemis/service/LearningGoalService.java
+++ b/src/main/java/de/tum/in/www1/artemis/service/CompetencyService.java
@@ -13,18 +13,18 @@
import de.tum.in.www1.artemis.web.rest.util.PageUtil;
@Service
-public class LearningGoalService {
+public class CompetencyService {
- private final LearningGoalRepository learningGoalRepository;
+ private final CompetencyRepository competencyRepository;
private final AuthorizationCheckService authCheckService;
- private final LearningGoalProgressService learningGoalProgressService;
+ private final CompetencyProgressService competencyProgressService;
- public LearningGoalService(LearningGoalRepository learningGoalRepository, AuthorizationCheckService authCheckService, LearningGoalProgressService learningGoalProgressService) {
- this.learningGoalRepository = learningGoalRepository;
+ public CompetencyService(CompetencyRepository competencyRepository, AuthorizationCheckService authCheckService, CompetencyProgressService competencyProgressService) {
+ this.competencyRepository = competencyRepository;
this.authCheckService = authCheckService;
- this.learningGoalProgressService = learningGoalProgressService;
+ this.competencyProgressService = competencyProgressService;
}
/**
@@ -35,14 +35,14 @@ public LearningGoalService(LearningGoalRepository learningGoalRepository, Author
* @param updateProgress Whether the competency progress should be updated or taken from the database.
* @return A list of competencies with their lecture units (filtered for the user) and user progress.
*/
- public Set findAllForCourse(@NotNull Course course, @NotNull User user, boolean updateProgress) {
+ public Set findAllForCourse(@NotNull Course course, @NotNull User user, boolean updateProgress) {
if (updateProgress) {
// Get the competencies with the updated progress for the specified user.
- return learningGoalProgressService.getLearningGoalsAndUpdateProgressByUserInCourse(user, course);
+ return competencyProgressService.getCompetenciesAndUpdateProgressByUserInCourse(user, course);
}
else {
// Fetch the competencies with the user progress from the database.
- return learningGoalRepository.findAllForCourseWithProgressForUser(course.getId(), user.getId());
+ return competencyRepository.findAllForCourseWithProgressForUser(course.getId(), user.getId());
}
}
@@ -53,10 +53,10 @@ public Set findAllForCourse(@NotNull Course course, @NotNull User
* @param user The user that is requesting the prerequisites.
* @return A list of prerequisites (without lecture units if student is not part of course).
*/
- public Set findAllPrerequisitesForCourse(@NotNull Course course, @NotNull User user) {
- Set prerequisites = learningGoalRepository.findPrerequisitesByCourseId(course.getId());
+ public Set findAllPrerequisitesForCourse(@NotNull Course course, @NotNull User user) {
+ Set prerequisites = competencyRepository.findPrerequisitesByCourseId(course.getId());
// Remove all lecture units
- for (LearningGoal prerequisite : prerequisites) {
+ for (Competency prerequisite : prerequisites) {
prerequisite.setLectureUnits(Collections.emptySet());
}
return prerequisites;
@@ -69,27 +69,27 @@ public Set findAllPrerequisitesForCourse(@NotNull Course course, @
* @param user The user for whom to fetch all available lectures
* @return A wrapper object containing a list of all found competencies and the total number of pages
*/
- public SearchResultPageDTO getAllOnPageWithSize(final PageableSearchDTO search, final User user) {
- final var pageable = PageUtil.createLearningGoalPageRequest(search);
+ public SearchResultPageDTO getAllOnPageWithSize(final PageableSearchDTO search, final User user) {
+ final var pageable = PageUtil.createCompetencyPageRequest(search);
final var searchTerm = search.getSearchTerm();
- final Page learningGoalPage;
+ final Page competencyPage;
if (authCheckService.isAdmin(user)) {
- learningGoalPage = learningGoalRepository.findByTitleIgnoreCaseContainingOrCourse_TitleIgnoreCaseContaining(searchTerm, searchTerm, pageable);
+ competencyPage = competencyRepository.findByTitleIgnoreCaseContainingOrCourse_TitleIgnoreCaseContaining(searchTerm, searchTerm, pageable);
}
else {
- learningGoalPage = learningGoalRepository.findByTitleInLectureOrCourseAndUserHasAccessToCourse(searchTerm, searchTerm, user.getGroups(), pageable);
+ competencyPage = competencyRepository.findByTitleInLectureOrCourseAndUserHasAccessToCourse(searchTerm, searchTerm, user.getGroups(), pageable);
}
- return new SearchResultPageDTO<>(learningGoalPage.getContent(), learningGoalPage.getTotalPages());
+ return new SearchResultPageDTO<>(competencyPage.getContent(), competencyPage.getTotalPages());
}
/**
* Checks if the provided competencies and relations between them contain a cycle
*
- * @param learningGoals The set of competencies that get checked for cycles
- * @param relations The set of relations that get checked for cycles
+ * @param competencies The set of competencies that get checked for cycles
+ * @param relations The set of relations that get checked for cycles
* @return A boolean that states whether the provided competencies and relations contain a cycle
*/
- public boolean doesCreateCircularRelation(Set learningGoals, Set relations) {
+ public boolean doesCreateCircularRelation(Set competencies, Set relations) {
// Inner class Vertex is only used in this method for cycle detection
class Vertex {
@@ -177,12 +177,12 @@ else if (!neighbor.isVisited() && vertexIsPartOfCycle(neighbor)) {
}
var graph = new Graph();
- for (LearningGoal learningGoal : learningGoals) {
- graph.addVertex(new Vertex(learningGoal.getTitle()));
+ for (Competency competency : competencies) {
+ graph.addVertex(new Vertex(competency.getTitle()));
}
- for (LearningGoalRelation relation : relations) {
- var headVertex = graph.vertices.stream().filter(vertex -> vertex.label.equals(relation.getHeadLearningGoal().getTitle())).findFirst().orElseThrow();
- var tailVertex = graph.vertices.stream().filter(vertex -> vertex.label.equals(relation.getTailLearningGoal().getTitle())).findFirst().orElseThrow();
+ for (CompetencyRelation relation : relations) {
+ var headVertex = graph.vertices.stream().filter(vertex -> vertex.label.equals(relation.getHeadCompetency().getTitle())).findFirst().orElseThrow();
+ var tailVertex = graph.vertices.stream().filter(vertex -> vertex.label.equals(relation.getTailCompetency().getTitle())).findFirst().orElseThrow();
// Only EXTENDS and ASSUMES are included in the generated graph as other relations are no problem if they are circular
// MATCHES relations are considered in the next step by merging the edges and combining the adjacencyLists
switch (relation.getType()) {
@@ -190,10 +190,10 @@ else if (!neighbor.isVisited() && vertexIsPartOfCycle(neighbor)) {
}
}
// combine vertices that are connected through MATCHES
- for (LearningGoalRelation relation : relations) {
- if (relation.getType() == LearningGoalRelation.RelationType.MATCHES) {
- var headVertex = graph.vertices.stream().filter(vertex -> vertex.label.equals(relation.getHeadLearningGoal().getTitle())).findFirst().orElseThrow();
- var tailVertex = graph.vertices.stream().filter(vertex -> vertex.label.equals(relation.getTailLearningGoal().getTitle())).findFirst().orElseThrow();
+ for (CompetencyRelation relation : relations) {
+ if (relation.getType() == CompetencyRelation.RelationType.MATCHES) {
+ var headVertex = graph.vertices.stream().filter(vertex -> vertex.label.equals(relation.getHeadCompetency().getTitle())).findFirst().orElseThrow();
+ var tailVertex = graph.vertices.stream().filter(vertex -> vertex.label.equals(relation.getTailCompetency().getTitle())).findFirst().orElseThrow();
if (headVertex.adjacencyList.contains(tailVertex) || tailVertex.adjacencyList.contains(headVertex)) {
return true;
}
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 193efe069fef..b641e3525a73 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
@@ -106,9 +106,9 @@ public class CourseService {
private final AuditEventRepository auditEventRepository;
- private final LearningGoalService learningGoalService;
+ private final CompetencyService competencyService;
- private final LearningGoalRepository learningGoalRepository;
+ private final CompetencyRepository competencyRepository;
private final GradingScaleRepository gradingScaleRepository;
@@ -149,8 +149,8 @@ public class CourseService {
public CourseService(Environment env, ArtemisAuthenticationProvider artemisAuthenticationProvider, CourseRepository courseRepository, ExerciseService exerciseService,
ExerciseDeletionService exerciseDeletionService, AuthorizationCheckService authCheckService, UserRepository userRepository, LectureService lectureService,
GroupNotificationRepository groupNotificationRepository, ExerciseGroupRepository exerciseGroupRepository, AuditEventRepository auditEventRepository,
- UserService userService, ExamDeletionService examDeletionService, LearningGoalRepository learningGoalRepository, GroupNotificationService groupNotificationService,
- ExamRepository examRepository, CourseExamExportService courseExamExportService, LearningGoalService learningGoalService, GradingScaleRepository gradingScaleRepository,
+ UserService userService, ExamDeletionService examDeletionService, CompetencyRepository competencyRepository, GroupNotificationService groupNotificationService,
+ ExamRepository examRepository, CourseExamExportService courseExamExportService, CompetencyService competencyService, GradingScaleRepository gradingScaleRepository,
StatisticsRepository statisticsRepository, StudentParticipationRepository studentParticipationRepository, TutorLeaderboardService tutorLeaderboardService,
RatingRepository ratingRepository, ComplaintService complaintService, ComplaintRepository complaintRepository, ResultRepository resultRepository,
ComplaintResponseRepository complaintResponseRepository, SubmissionRepository submissionRepository, ProgrammingExerciseRepository programmingExerciseRepository,
@@ -170,11 +170,11 @@ public CourseService(Environment env, ArtemisAuthenticationProvider artemisAuthe
this.auditEventRepository = auditEventRepository;
this.userService = userService;
this.examDeletionService = examDeletionService;
- this.learningGoalRepository = learningGoalRepository;
+ this.competencyRepository = competencyRepository;
this.groupNotificationService = groupNotificationService;
this.examRepository = examRepository;
this.courseExamExportService = courseExamExportService;
- this.learningGoalService = learningGoalService;
+ this.competencyService = competencyService;
this.gradingScaleRepository = gradingScaleRepository;
this.statisticsRepository = statisticsRepository;
this.studentParticipationRepository = studentParticipationRepository;
@@ -246,7 +246,7 @@ public void fetchPlagiarismCasesForCourseExercises(Set exercises, Long
* @param refresh if the user requested an explicit refresh
* @return the course including exercises, lectures, exams, competencies and tutorial groups (filtered for given user)
*/
- public Course findOneWithExercisesAndLecturesAndExamsAndLearningGoalsAndTutorialGroupsForUser(Long courseId, User user, boolean refresh) {
+ public Course findOneWithExercisesAndLecturesAndExamsAndCompetenciesAndTutorialGroupsForUser(Long courseId, User user, boolean refresh) {
Course course = courseRepository.findByIdWithLecturesElseThrow(courseId);
// Load exercises with categories separately because this is faster than loading them with lectures and exam above (the query would become too complex)
course.setExercises(exerciseRepository.findByCourseIdWithCategories(course.getId()));
@@ -254,8 +254,8 @@ public Course findOneWithExercisesAndLecturesAndExamsAndLearningGoalsAndTutorial
exerciseService.loadExerciseDetailsIfNecessary(course, user);
course.setExams(examRepository.findByCourseIdsForUser(Set.of(course.getId()), user.getId(), user.getGroups(), ZonedDateTime.now()));
course.setLectures(lectureService.filterActiveAttachments(course.getLectures(), user));
- course.setLearningGoals(learningGoalService.findAllForCourse(course, user, refresh));
- course.setPrerequisites(learningGoalService.findAllPrerequisitesForCourse(course, user));
+ course.setCompetencies(competencyService.findAllForCourse(course, user, refresh));
+ course.setPrerequisites(competencyService.findAllPrerequisitesForCourse(course, user));
course.setTutorialGroups(tutorialGroupService.findAllForCourse(course, user));
course.setTutorialGroupsConfiguration(tutorialGroupsConfigurationRepository.findByCourseIdWithEagerTutorialGroupFreePeriods(courseId).orElse(null));
if (authCheckService.isOnlyStudentInCourse(course, user)) {
@@ -346,7 +346,7 @@ public void delete(Course course) {
deleteExercisesOfCourse(course);
deleteLecturesOfCourse(course);
- deleteLearningGoalsOfCourse(course);
+ deleteCompetenciesOfCourse(course);
deleteConversationsOfCourse(course);
deleteNotificationsOfCourse(course);
deleteDefaultGroups(course);
@@ -419,9 +419,9 @@ private void deleteExercisesOfCourse(Course course) {
}
}
- private void deleteLearningGoalsOfCourse(Course course) {
- for (LearningGoal learningGoal : course.getLearningGoals()) {
- learningGoalRepository.deleteById(learningGoal.getId());
+ private void deleteCompetenciesOfCourse(Course course) {
+ for (Competency competency : course.getCompetencies()) {
+ competencyRepository.deleteById(competency.getId());
}
}
diff --git a/src/main/java/de/tum/in/www1/artemis/service/ExerciseDeletionService.java b/src/main/java/de/tum/in/www1/artemis/service/ExerciseDeletionService.java
index 6eed7d0a3a3c..a7538e5cc629 100644
--- a/src/main/java/de/tum/in/www1/artemis/service/ExerciseDeletionService.java
+++ b/src/main/java/de/tum/in/www1/artemis/service/ExerciseDeletionService.java
@@ -113,7 +113,7 @@ public void cleanup(Long exerciseId, boolean deleteRepositories) {
* all other exercise types)
*/
public void delete(long exerciseId, boolean deleteStudentReposBuildPlans, boolean deleteBaseReposBuildPlans) {
- var exercise = exerciseRepository.findByIdWithLearningGoalsElseThrow(exerciseId);
+ var exercise = exerciseRepository.findByIdWithCompetenciesElseThrow(exerciseId);
log.info("Request to delete {} with id {}", exercise.getClass().getSimpleName(), exerciseId);
if (exercise instanceof ModelingExercise modelingExercise) {
@@ -130,7 +130,7 @@ public void delete(long exerciseId, boolean deleteStudentReposBuildPlans, boolea
}
// delete all exercise units linking to the exercise
- List exerciseUnits = this.exerciseUnitRepository.findByIdWithLearningGoalsBidirectional(exerciseId);
+ List exerciseUnits = this.exerciseUnitRepository.findByIdWithCompetenciesBidirectional(exerciseId);
for (ExerciseUnit exerciseUnit : exerciseUnits) {
lectureUnitService.removeLectureUnit(exerciseUnit);
}
diff --git a/src/main/java/de/tum/in/www1/artemis/service/LectureUnitService.java b/src/main/java/de/tum/in/www1/artemis/service/LectureUnitService.java
index d6a10b425d8f..71525a263a97 100644
--- a/src/main/java/de/tum/in/www1/artemis/service/LectureUnitService.java
+++ b/src/main/java/de/tum/in/www1/artemis/service/LectureUnitService.java
@@ -8,13 +8,13 @@
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
-import de.tum.in.www1.artemis.domain.LearningGoal;
+import de.tum.in.www1.artemis.domain.Competency;
import de.tum.in.www1.artemis.domain.Lecture;
import de.tum.in.www1.artemis.domain.User;
import de.tum.in.www1.artemis.domain.lecture.ExerciseUnit;
import de.tum.in.www1.artemis.domain.lecture.LectureUnit;
import de.tum.in.www1.artemis.domain.lecture.LectureUnitCompletion;
-import de.tum.in.www1.artemis.repository.LearningGoalRepository;
+import de.tum.in.www1.artemis.repository.CompetencyRepository;
import de.tum.in.www1.artemis.repository.LectureRepository;
import de.tum.in.www1.artemis.repository.LectureUnitCompletionRepository;
import de.tum.in.www1.artemis.repository.LectureUnitRepository;
@@ -26,15 +26,15 @@ public class LectureUnitService {
private final LectureRepository lectureRepository;
- private final LearningGoalRepository learningGoalRepository;
+ private final CompetencyRepository competencyRepository;
private final LectureUnitCompletionRepository lectureUnitCompletionRepository;
- public LectureUnitService(LectureUnitRepository lectureUnitRepository, LectureRepository lectureRepository, LearningGoalRepository learningGoalRepository,
+ public LectureUnitService(LectureUnitRepository lectureUnitRepository, LectureRepository lectureRepository, CompetencyRepository competencyRepository,
LectureUnitCompletionRepository lectureUnitCompletionRepository) {
this.lectureUnitRepository = lectureUnitRepository;
this.lectureRepository = lectureRepository;
- this.learningGoalRepository = learningGoalRepository;
+ this.competencyRepository = competencyRepository;
this.lectureUnitCompletionRepository = lectureUnitCompletionRepository;
}
@@ -76,15 +76,15 @@ public void setLectureUnitCompletion(@NotNull LectureUnit lectureUnit, @NotNull
* @param lectureUnit lecture unit to delete
*/
public void removeLectureUnit(@NotNull LectureUnit lectureUnit) {
- LectureUnit lectureUnitToDelete = lectureUnitRepository.findByIdWithLearningGoalsElseThrow(lectureUnit.getId());
+ LectureUnit lectureUnitToDelete = lectureUnitRepository.findByIdWithCompetenciesElseThrow(lectureUnit.getId());
if (!(lectureUnitToDelete instanceof ExerciseUnit)) {
// update associated competencies
- Set learningGoals = lectureUnitToDelete.getLearningGoals();
- learningGoalRepository.saveAll(learningGoals.stream().map(learningGoal -> {
- learningGoal = learningGoalRepository.findByIdWithLectureUnitsElseThrow(learningGoal.getId());
- learningGoal.getLectureUnits().remove(lectureUnitToDelete);
- return learningGoal;
+ Set competencies = lectureUnitToDelete.getCompetencies();
+ competencyRepository.saveAll(competencies.stream().map(competency -> {
+ competency = competencyRepository.findByIdWithLectureUnitsElseThrow(competency.getId());
+ competency.getLectureUnits().remove(lectureUnitToDelete);
+ return competency;
}).toList());
}
diff --git a/src/main/java/de/tum/in/www1/artemis/service/ProfileService.java b/src/main/java/de/tum/in/www1/artemis/service/ProfileService.java
new file mode 100644
index 000000000000..17b06f4dd79a
--- /dev/null
+++ b/src/main/java/de/tum/in/www1/artemis/service/ProfileService.java
@@ -0,0 +1,31 @@
+package de.tum.in.www1.artemis.service;
+
+import java.util.Set;
+
+import org.springframework.core.env.Environment;
+import org.springframework.stereotype.Service;
+
+import de.tum.in.www1.artemis.config.Constants;
+import tech.jhipster.config.JHipsterConstants;
+
+@Service
+public class ProfileService {
+
+ private final Environment environment;
+
+ public ProfileService(Environment environment) {
+ this.environment = environment;
+ }
+
+ public boolean isDev() {
+ return isProfileActive(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT);
+ }
+
+ public boolean isLocalVcsCi() {
+ return isProfileActive(Constants.PROFILE_LOCALVC) || isProfileActive(Constants.PROFILE_LOCALCI);
+ }
+
+ private boolean isProfileActive(String profile) {
+ return Set.of(this.environment.getActiveProfiles()).contains(profile);
+ }
+}
diff --git a/src/main/java/de/tum/in/www1/artemis/service/RepositoryAccessService.java b/src/main/java/de/tum/in/www1/artemis/service/RepositoryAccessService.java
index 0fc8af4fe522..52e6be714117 100644
--- a/src/main/java/de/tum/in/www1/artemis/service/RepositoryAccessService.java
+++ b/src/main/java/de/tum/in/www1/artemis/service/RepositoryAccessService.java
@@ -58,28 +58,37 @@ public void checkAccessRepositoryElseThrow(ProgrammingExerciseParticipation prog
throw new AccessUnauthorizedException();
}
+ boolean isAtLeastEditor = authorizationCheckService.isAtLeastEditorInCourse(programmingExercise.getCourseViaExerciseGroupOrCourseMember(), user);
+ boolean isStudent = authorizationCheckService.isOnlyStudentInCourse(programmingExercise.getCourseViaExerciseGroupOrCourseMember(), user);
+ boolean isTeachingAssistant = !isStudent && !isAtLeastEditor;
+
// Error case 2: The user's participation repository is locked.
boolean lockRepositoryPolicyEnforced = false;
if (programmingExercise.getSubmissionPolicy() instanceof LockRepositoryPolicy policy) {
lockRepositoryPolicyEnforced = submissionPolicyService.isParticipationLocked(policy, (Participation) programmingParticipation);
}
// Editors and up are able to push to any repository even if the participation is locked for the student.
- boolean isAtLeastEditor = authorizationCheckService.isAtLeastEditorInCourse(programmingExercise.getCourseViaExerciseGroupOrCourseMember(), user);
- if (repositoryActionType == RepositoryActionType.WRITE && !isAtLeastEditor && (programmingParticipation.isLocked() || lockRepositoryPolicyEnforced)) {
+ // Teaching assistants trying to push to a student assignment repository will be blocked by the next check.
+ if (repositoryActionType == RepositoryActionType.WRITE && isStudent && (programmingParticipation.isLocked() || lockRepositoryPolicyEnforced)) {
throw new AccessForbiddenException();
}
- boolean isStudent = authorizationCheckService.isOnlyStudentInCourse(programmingExercise.getCourseViaExerciseGroupOrCourseMember(), user);
+ // Error case 3: A teaching assistant tries to push into a base repository (in that case the participation is not a StudentParticipation) or into a student assignment
+ // repository (in that case the teaching assistant does not own the participation).
+ boolean isStudentParticipation = programmingParticipation instanceof StudentParticipation;
+ if (isTeachingAssistant && repositoryActionType == RepositoryActionType.WRITE
+ && (!isStudentParticipation || !((StudentParticipation) programmingParticipation).isOwnedBy(user))) {
+ throw new AccessUnauthorizedException();
+ }
- // Error case 3: The student can reset the repository only before and a tutor/instructor only after the due date has passed
+ // Error case 4: The student can reset the repository only before and a tutor/instructor only after the due date has passed
if (repositoryActionType == RepositoryActionType.RESET) {
checkAccessRepositoryForReset(programmingParticipation, isStudent, programmingExercise);
}
- // Error case 4: Before or after exam working time, students are not allowed to read or submit to the repository for an exam exercise. Teaching assistants are only allowed
+ // Error case 5: Before or after exam working time, students are not allowed to read or submit to the repository for an exam exercise. Teaching assistants are only allowed
// to read the student's repository.
// But the student should still be able to access if they are notified for a related plagiarism case.
- boolean isTeachingAssistant = !isStudent && !isAtLeastEditor;
if ((isStudent || (isTeachingAssistant && repositoryActionType != RepositoryActionType.READ))
&& !examSubmissionService.isAllowedToSubmitDuringExam(programmingExercise, user, false) && !userWasNotifiedAboutPlagiarismCase) {
throw new AccessForbiddenException();
@@ -127,8 +136,6 @@ else if (!isStudent && !isOwner) {
* @param user the user that wants to access the test repository.
*/
public void checkAccessTestRepositoryElseThrow(boolean atLeastEditor, ProgrammingExercise exercise, User user) {
- // The only test-repository endpoints that require at least editor permissions are the getStatus (GET /api/test-repository/{exerciseId}) and updateTestFiles (PUT
- // /api/test-repository/{exerciseId}/files) endpoints.
if (atLeastEditor) {
if (!authorizationCheckService.isAtLeastEditorInCourse(exercise.getCourseViaExerciseGroupOrCourseMember(), user)) {
throw new AccessForbiddenException("You are not allowed to access the test repository of this programming exercise.");
diff --git a/src/main/java/de/tum/in/www1/artemis/service/ResourceLoaderService.java b/src/main/java/de/tum/in/www1/artemis/service/ResourceLoaderService.java
index a124ff24930f..42f7936fa5e9 100644
--- a/src/main/java/de/tum/in/www1/artemis/service/ResourceLoaderService.java
+++ b/src/main/java/de/tum/in/www1/artemis/service/ResourceLoaderService.java
@@ -2,10 +2,17 @@
import java.io.File;
import java.io.IOException;
+import java.io.InputStream;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Files;
import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
+import java.util.UUID;
import javax.annotation.Nonnull;
@@ -168,4 +175,40 @@ private Path resolveResourcePath(final Path resource) {
private boolean isOverrideAllowed(final Path path) {
return ALLOWED_OVERRIDE_PREFIXES.stream().anyMatch(path::startsWith);
}
+
+ /**
+ * Get the path to a file in the 'resources' folder.
+ * If the file is in the file system, the path to the file is returned.
+ * If the file is in a jar file, the file is extracted to a temporary file and the path to the temporary file is returned.
+ *
+ * @param path the path to the file in the 'resources' folder.
+ * @return the path to the file in the file system or in the jar file.
+ */
+ public Path getResourceFilePath(Path path) throws IOException, URISyntaxException {
+
+ Resource resource = getResource(path);
+
+ if (!resource.exists()) {
+ throw new IOException("Resource does not exist: " + path);
+ }
+
+ URL resourceUrl = resource.getURL();
+
+ if ("file".equals(resourceUrl.getProtocol())) {
+ // Resource is in the file system.
+ return Paths.get(resourceUrl.toURI());
+ }
+ else if ("jar".equals(resourceUrl.getProtocol())) {
+ // Resource is in a jar file.
+ InputStream resourceInputStream = resource.getInputStream();
+
+ Path resourcePath = Files.createTempFile(UUID.randomUUID().toString(), "");
+ Files.copy(resourceInputStream, resourcePath, StandardCopyOption.REPLACE_EXISTING);
+ resourceInputStream.close();
+ // Delete the temporary file when the JVM exits.
+ resourcePath.toFile().deleteOnExit();
+ return resourcePath;
+ }
+ throw new IllegalArgumentException("Unsupported protocol: " + resourceUrl.getProtocol());
+ }
}
diff --git a/src/main/java/de/tum/in/www1/artemis/service/UrlService.java b/src/main/java/de/tum/in/www1/artemis/service/UrlService.java
index eae9a43d1adb..5bd6d22d6a66 100644
--- a/src/main/java/de/tum/in/www1/artemis/service/UrlService.java
+++ b/src/main/java/de/tum/in/www1/artemis/service/UrlService.java
@@ -45,7 +45,6 @@ public String getRepositorySlugFromRepositoryUrlString(String repositoryUrl) thr
/**
* Gets the repository slug from the given URL
- *
* Example 1: https://ga42xab@bitbucket.ase.in.tum.de/scm/EIST2016RME/RMEXERCISE-ga42xab.git --> RMEXERCISE-ga42xab
* Example 2: https://ga63fup@repobruegge.in.tum.de/scm/EIST2016RME/RMEXERCISE-ga63fup.git --> RMEXERCISE-ga63fup
* Example 3: https://artemistest2gitlab.ase.in.tum.de/TESTADAPTER/testadapter-exercise.git --> testadapter-exercise
@@ -61,7 +60,7 @@ private String getRepositorySlugFromUrl(URI url) throws VersionControlException
if (pathComponents.length < 2) {
throw new VersionControlException("Repository URL is not a git URL! Can't get repository slug for " + url);
}
- // Note: pathComponents[] = "" because the path always starts with "/"
+ // Note: pathComponents[0] = "" because the path always starts with "/"
// take the last element
String repositorySlug = pathComponents[pathComponents.length - 1];
// if the element includes ".git" ...
@@ -98,7 +97,7 @@ private String getRepositoryPathFromUrl(URI url) throws VersionControlException
if (pathComponents.length < 2) {
throw new VersionControlException("Repository URL is not a git URL! Can't get repository slug for " + url);
}
- // Note: pathComponents[] = "" because the path always starts with "/"
+ // Note: pathComponents[0] = "" because the path always starts with "/"
final var last = pathComponents.length - 1;
return pathComponents[last - 1] + "/" + pathComponents[last].replace(".git", "");
}
@@ -117,7 +116,9 @@ public String getProjectKeyFromRepositoryUrl(VcsRepositoryUrl repositoryUrl) thr
/**
* Gets the project key from the given URL
*
- * Example: https://ga42xab@bitbucket.ase.in.tum.de/scm/EIST2016RME/RMEXERCISE-ga42xab.git --> EIST2016RME
+ * Examples:
+ * https://ga42xab@bitbucket.ase.in.tum.de/scm/EIST2016RME/RMEXERCISE-ga42xab.git --> EIST2016RME
+ * http://localhost:8080/git/TESTCOURSE1TESTEX1/testcourse1testex1-student1.git --> TESTCOURSE1TESTEX1
*
* @param url The complete repository url (including protocol, host and the complete path)
* @return The project key
@@ -129,10 +130,10 @@ private String getProjectKeyFromUrl(URI url) throws VersionControlException {
if (pathComponents.length <= 2) {
throw new VersionControlException("No project key could be found for " + url);
}
- // Note: pathComponents[] = "" because the path always starts with "/"
+ // Note: pathComponents[0] = "" because the path always starts with "/"
var projectKey = pathComponents[1];
- if ("scm".equals(pathComponents[1])) {
- // special case for Bitbucket
+ if ("scm".equals(pathComponents[1]) || "git".equals(pathComponents[1])) {
+ // special case for Bitbucket and local VC
projectKey = pathComponents[2];
}
return projectKey;
diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/GitService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/GitService.java
index 4ebf9be81e47..2e8d658da583 100644
--- a/src/main/java/de/tum/in/www1/artemis/service/connectors/GitService.java
+++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/GitService.java
@@ -42,6 +42,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -52,7 +53,9 @@
import de.tum.in.www1.artemis.domain.participation.StudentParticipation;
import de.tum.in.www1.artemis.exception.GitException;
import de.tum.in.www1.artemis.service.FileService;
+import de.tum.in.www1.artemis.service.ProfileService;
import de.tum.in.www1.artemis.service.ZipFileService;
+import de.tum.in.www1.artemis.service.connectors.localvc.LocalVCRepositoryUrl;
import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException;
@Service
@@ -60,6 +63,10 @@ public class GitService {
private final Logger log = LoggerFactory.getLogger(GitService.class);
+ private final Environment environment;
+
+ private final ProfileService profileService;
+
@Value("${artemis.version-control.url}")
private URL gitUrl;
@@ -111,11 +118,13 @@ public class GitService {
private static final String REMOTE_NAME = "origin";
- public GitService(FileService fileService, ZipFileService zipFileService) {
+ public GitService(Environment environment, ProfileService profileService, FileService fileService, ZipFileService zipFileService) {
+ this.profileService = profileService;
log.info("file.encoding={}", System.getProperty("file.encoding"));
log.info("sun.jnu.encoding={}", System.getProperty("sun.jnu.encoding"));
log.info("Default Charset={}", Charset.defaultCharset());
log.info("Default Charset in Use={}", new OutputStreamWriter(new ByteArrayOutputStream()).getEncoding());
+ this.environment = environment;
this.fileService = fileService;
this.zipFileService = zipFileService;
}
@@ -252,7 +261,24 @@ private String getGitUriAsString(VcsRepositoryUrl vcsRepositoryUrl) throws URISy
return getGitUri(vcsRepositoryUrl).toString();
}
+ /**
+ * Get the URI for a {@link VcsRepositoryUrl}. This either retrieves the SSH URI, if SSH is used, the HTTP(S) URI, or the path to the repository's folder if the local VCS is
+ * used.
+ * This method is for internal use (getting the URI for cloning the repository into the Artemis file system).
+ * For Bitbucket and GitLab, the URI is the same internally as the one that is used by the students to clone the repository using their local Git client.
+ * For the local VCS however, the repository is cloned from the folder defined in the environment variable "artemis.version-control.local-vcs-repo-path".
+ *
+ * @param vcsRepositoryUrl the {@link VcsRepositoryUrl} for which to get the URI
+ * @return the URI (SSH, HTTP(S), or local path)
+ * @throws URISyntaxException if SSH is used and the SSH URI could not be retrieved.
+ */
private URI getGitUri(VcsRepositoryUrl vcsRepositoryUrl) throws URISyntaxException {
+ if (profileService.isLocalVcsCi()) {
+ // Create less generic LocalVCRepositoryUrl out of VcsRepositoryUrl.
+ LocalVCRepositoryUrl localVCRepositoryUrl = new LocalVCRepositoryUrl(vcsRepositoryUrl.toString(), gitUrl);
+ String localVCBasePath = environment.getProperty("artemis.version-control.local-vcs-repo-path");
+ return localVCRepositoryUrl.getLocalRepositoryPath(localVCBasePath).toUri();
+ }
return useSsh() ? getSshUri(vcsRepositoryUrl) : vcsRepositoryUrl.getURI();
}
diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIBuildJobExecutionService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIBuildJobExecutionService.java
new file mode 100644
index 000000000000..7a7626ed743f
--- /dev/null
+++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIBuildJobExecutionService.java
@@ -0,0 +1,361 @@
+package de.tum.in.www1.artemis.service.connectors.localci;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
+import org.apache.commons.io.IOUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Service;
+
+import com.github.dockerjava.api.DockerClient;
+import com.github.dockerjava.api.command.CreateContainerResponse;
+import com.github.dockerjava.api.exception.NotFoundException;
+import com.github.dockerjava.api.model.HostConfig;
+
+import de.tum.in.www1.artemis.config.localvcci.LocalCIConfiguration;
+import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation;
+import de.tum.in.www1.artemis.exception.LocalCIException;
+import de.tum.in.www1.artemis.exception.localvc.LocalVCInternalException;
+import de.tum.in.www1.artemis.service.connectors.ci.ContinuousIntegrationService;
+import de.tum.in.www1.artemis.service.connectors.localci.dto.LocalCIBuildResult;
+import de.tum.in.www1.artemis.service.connectors.localvc.LocalVCRepositoryUrl;
+import de.tum.in.www1.artemis.service.connectors.vcs.VersionControlService;
+import de.tum.in.www1.artemis.service.util.TimeLogUtil;
+
+/**
+ * This service contains the logic to execute a build job for a programming exercise participation in the local CI system.
+ * The {@link #runBuildJob(ProgrammingExerciseParticipation, String, String)} method is wrapped into a Callable by the {@link LocalCIBuildJobManagementService} and submitted to the
+ * executor service.
+ */
+@Service
+@Profile("localci")
+public class LocalCIBuildJobExecutionService {
+
+ private final Logger log = LoggerFactory.getLogger(LocalCIBuildJobExecutionService.class);
+
+ private final LocalCIBuildPlanService localCIBuildPlanService;
+
+ private final Optional versionControlService;
+
+ private final DockerClient dockerClient;
+
+ private final LocalCIContainerService localCIContainerService;
+
+ /**
+ * Instead of creating a new XMLInputFactory for every build job, it is created once and provided as a Bean (see {@link LocalCIConfiguration#localCIXMLInputFactory()}).
+ */
+ private final XMLInputFactory localCIXMLInputFactory;
+
+ @Value("${artemis.version-control.url}")
+ private URL localVCBaseUrl;
+
+ @Value("${artemis.version-control.local-vcs-repo-path}")
+ private String localVCBasePath;
+
+ public LocalCIBuildJobExecutionService(LocalCIBuildPlanService localCIBuildPlanService, Optional versionControlService, DockerClient dockerClient,
+ LocalCIContainerService localCIContainerService, XMLInputFactory localCIXMLInputFactory) {
+ this.localCIBuildPlanService = localCIBuildPlanService;
+ this.versionControlService = versionControlService;
+ this.dockerClient = dockerClient;
+ this.localCIContainerService = localCIContainerService;
+ this.localCIXMLInputFactory = localCIXMLInputFactory;
+ }
+
+ public enum LocalCIBuildJobRepositoryType {
+
+ ASSIGNMENT("assignment"), TEST("test");
+
+ private final String name;
+
+ LocalCIBuildJobRepositoryType(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+ }
+
+ /**
+ * Prepare the paths to the assignment and test repositories, the branch to checkout, the volume configuration for the Docker container, and the container configuration,
+ * and then call {@link #runScriptAndParseResults(ProgrammingExerciseParticipation, String, String, String, String)} to execute the job.
+ *
+ * @param participation The participation of the repository for which the build job should be executed.
+ * @param commitHash The commit hash of the commit that should be built. If it is null, the latest commit of the default branch will be built.
+ * @param containerName The name of the Docker container that will be used to run the build job.
+ * It needs to be prepared beforehand to stop and remove the container if something goes wrong here.
+ * @return The build result.
+ * @throws LocalCIException If some error occurs while preparing or running the build job.
+ */
+ public LocalCIBuildResult runBuildJob(ProgrammingExerciseParticipation participation, String commitHash, String containerName) {
+ // Update the build plan status to "BUILDING".
+ localCIBuildPlanService.updateBuildPlanStatus(participation, ContinuousIntegrationService.BuildStatus.BUILDING);
+
+ // Retrieve the paths to the repositories that the build job needs.
+ // This includes the assignment repository (the one to be tested, e.g. the student's repository, or the template repository), and the tests repository which includes
+ // the tests to be executed.
+ LocalVCRepositoryUrl assignmentRepositoryUrl;
+ LocalVCRepositoryUrl testsRepositoryUrl;
+ try {
+ assignmentRepositoryUrl = new LocalVCRepositoryUrl(participation.getRepositoryUrl(), localVCBaseUrl);
+ testsRepositoryUrl = new LocalVCRepositoryUrl(participation.getProgrammingExercise().getTestRepositoryUrl(), localVCBaseUrl);
+ }
+ catch (LocalVCInternalException e) {
+ throw new LocalCIException("Error while creating LocalVCRepositoryUrl", e);
+ }
+
+ Path assignmentRepositoryPath = assignmentRepositoryUrl.getLocalRepositoryPath(localVCBasePath).toAbsolutePath();
+ Path testsRepositoryPath = testsRepositoryUrl.getLocalRepositoryPath(localVCBasePath).toAbsolutePath();
+
+ String branch;
+ try {
+ branch = versionControlService.orElseThrow().getOrRetrieveBranchOfParticipation(participation);
+ }
+ catch (LocalVCInternalException e) {
+ throw new LocalCIException("Error while getting branch of participation", e);
+ }
+
+ // Create the volume configuration for the container. The assignment repository, the tests repository, and the build script are bound into the container to be used by
+ // the build job.
+ HostConfig volumeConfig = localCIContainerService.createVolumeConfig(assignmentRepositoryPath, testsRepositoryPath);
+
+ // Create the container from the "ls1tum/artemis-maven-template" image with the local paths to the Git repositories and the shell script bound to it. Also give the
+ // container information about the branch and commit hash to be used.
+ // This does not start the container yet.
+ CreateContainerResponse container = localCIContainerService.configureContainer(containerName, volumeConfig, branch, commitHash);
+
+ return runScriptAndParseResults(participation, containerName, container.getId(), branch, commitHash);
+ }
+
+ /**
+ * Runs the build job. This includes creating and starting a Docker container, executing the build script, and processing the build result.
+ *
+ * @param participation The participation for which the build job should be run.
+ * @param containerName The name of the container that should be used for the build job. This is used to remove the container and is also accessible from outside build job
+ * running in its own thread.
+ * @param containerId The id of the container that should be used for the build job.
+ * @param branch The branch that should be built.
+ * @param commitHash The commit hash of the commit that should be built. If it is null, this method uses the latest commit of the repository.
+ * @return The build result.
+ * @throws LocalCIException if something went wrong while running the build job.
+ */
+ private LocalCIBuildResult runScriptAndParseResults(ProgrammingExerciseParticipation participation, String containerName, String containerId, String branch,
+ String commitHash) {
+
+ long timeNanoStart = System.nanoTime();
+
+ localCIContainerService.startContainer(containerId);
+
+ log.info("Started container for build job " + containerName);
+
+ localCIContainerService.runScriptInContainer(containerId);
+
+ log.info("Finished running the build script in container " + containerName);
+
+ ZonedDateTime buildCompletedDate = ZonedDateTime.now();
+
+ String assignmentRepoCommitHash = commitHash;
+ String testRepoCommitHash = "";
+
+ try {
+ if (commitHash == null) {
+ // Retrieve the latest commit hash from the assignment repository.
+ assignmentRepoCommitHash = localCIContainerService.getCommitHashOfBranch(containerId, LocalCIBuildJobRepositoryType.ASSIGNMENT, branch);
+ }
+ // Always use the latest commit from the test repository.
+ testRepoCommitHash = localCIContainerService.getCommitHashOfBranch(containerId, LocalCIBuildJobRepositoryType.TEST, branch);
+ }
+ catch (NotFoundException | IOException e) {
+ // Could not read commit hash from .git folder. Stop the container and return a build result that indicates that the build failed (empty list for failed tests and
+ // empty list for successful tests).
+ localCIContainerService.stopContainer(containerName);
+ return constructFailedBuildResult(branch, assignmentRepoCommitHash, testRepoCommitHash, buildCompletedDate);
+ }
+
+ // When Gradle is used as the build tool, the test results are located in /repositories/test-repository/build/test-results/test/TEST-*.xml.
+ // When Maven is used as the build tool, the test results are located in /repositories/test-repository/target/surefire-reports/TEST-*.xml.
+ String testResultsPath = "/repositories/test-repository/build/test-results/test";
+
+ // Get an input stream of the test result files.
+ TarArchiveInputStream testResultsTarInputStream;
+ try {
+ testResultsTarInputStream = new TarArchiveInputStream(dockerClient.copyArchiveFromContainerCmd(containerId, testResultsPath).exec());
+ }
+ catch (NotFoundException e) {
+ // If the test results are not found, this means that something went wrong during the build and testing of the submission.
+ // Stop the container and return a build results that indicates that the build failed.
+ localCIContainerService.stopContainer(containerName);
+ return constructFailedBuildResult(branch, assignmentRepoCommitHash, testRepoCommitHash, buildCompletedDate);
+ }
+
+ localCIContainerService.stopContainer(containerName);
+
+ LocalCIBuildResult buildResult;
+ try {
+ buildResult = parseTestResults(testResultsTarInputStream, branch, assignmentRepoCommitHash, testRepoCommitHash, buildCompletedDate);
+ }
+ catch (IOException | XMLStreamException | IllegalStateException e) {
+ throw new LocalCIException("Error while parsing test results", e);
+ }
+
+ // Set the build status to "INACTIVE" to indicate that the build is not running anymore.
+ localCIBuildPlanService.updateBuildPlanStatus(participation, ContinuousIntegrationService.BuildStatus.INACTIVE);
+
+ log.info("Building and testing submission for repository {} and commit hash {} took {}", participation.getRepositoryUrl(), commitHash,
+ TimeLogUtil.formatDurationFrom(timeNanoStart));
+
+ return buildResult;
+ }
+
+ // --- Helper methods ----
+
+ private LocalCIBuildResult parseTestResults(TarArchiveInputStream testResultsTarInputStream, String assignmentRepoBranchName, String assignmentRepoCommitHash,
+ String testsRepoCommitHash, ZonedDateTime buildCompletedDate) throws IOException, XMLStreamException {
+
+ List failedTests = new ArrayList<>();
+ List successfulTests = new ArrayList<>();
+
+ TarArchiveEntry tarEntry;
+ while ((tarEntry = testResultsTarInputStream.getNextTarEntry()) != null) {
+
+ // Go through all tar entries that are test result files.
+ if (!isValidTestResultFile(tarEntry)) {
+ continue;
+ }
+
+ // Read the contents of the tar entry as a string.
+ String xmlString = readTarEntryContent(testResultsTarInputStream);
+
+ processTestResultFile(xmlString, failedTests, successfulTests);
+ }
+
+ return constructBuildResult(failedTests, successfulTests, assignmentRepoBranchName, assignmentRepoCommitHash, testsRepoCommitHash, !failedTests.isEmpty(),
+ buildCompletedDate);
+ }
+
+ private boolean isValidTestResultFile(TarArchiveEntry tarArchiveEntry) {
+ return !tarArchiveEntry.isDirectory() && tarArchiveEntry.getName().endsWith(".xml") && tarArchiveEntry.getName().startsWith("test/TEST-")
+ && tarArchiveEntry.getName().endsWith(".xml");
+ }
+
+ private String readTarEntryContent(TarArchiveInputStream tarArchiveInputStream) throws IOException {
+ return IOUtils.toString(tarArchiveInputStream, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * Processes a test result file and adds the failed and successful tests to the corresponding lists.
+ *
+ * @param testResultFileString The string that represents the test results XML file.
+ * @param failedTests The list of failed tests.
+ * @param successfulTests The list of successful tests.
+ * @throws XMLStreamException if the XML stream reader cannot be created or there is an error while parsing the XML file
+ * @throws IllegalStateException if the first start element of the XML file is not a "testsuite" node
+ */
+ private void processTestResultFile(String testResultFileString, List failedTests,
+ List successfulTests) throws XMLStreamException {
+ // Create an XML stream reader for the string that represents the test results XML file.
+ XMLStreamReader xmlStreamReader = localCIXMLInputFactory.createXMLStreamReader(new StringReader(testResultFileString));
+
+ // Move to the first start element.
+ while (xmlStreamReader.hasNext() && !xmlStreamReader.isStartElement()) {
+ xmlStreamReader.next();
+ }
+
+ // Check if the start element is the "testsuite" node.
+ if (!("testsuite".equals(xmlStreamReader.getLocalName()))) {
+ throw new IllegalStateException("Expected testsuite element, but got " + xmlStreamReader.getLocalName());
+ }
+
+ // Go through all testcase nodes.
+ while (xmlStreamReader.hasNext()) {
+ xmlStreamReader.next();
+
+ if (!xmlStreamReader.isStartElement() || !("testcase".equals(xmlStreamReader.getLocalName()))) {
+ continue;
+ }
+
+ // Now we are at the start of a "testcase" node.
+ processTestCaseNode(xmlStreamReader, failedTests, successfulTests);
+ }
+
+ // Close the XML stream reader.
+ xmlStreamReader.close();
+ }
+
+ private void processTestCaseNode(XMLStreamReader xmlStreamReader, List failedTests,
+ List successfulTests) throws XMLStreamException {
+ // Extract the name attribute from the "testcase" node. This is the name of the test case.
+ String name = xmlStreamReader.getAttributeValue(null, "name");
+
+ // Check if there is a failure node inside the testcase node.
+ // Call next() until there is an end element (no failure node exists inside the testcase node) or a start element (failure node exists inside the
+ // testcase node).
+ xmlStreamReader.next();
+ while (!(xmlStreamReader.isEndElement() || xmlStreamReader.isStartElement())) {
+ xmlStreamReader.next();
+ }
+ if (xmlStreamReader.isStartElement() && "failure".equals(xmlStreamReader.getLocalName())) {
+ // Extract the message attribute from the "failure" node.
+ String error = xmlStreamReader.getAttributeValue(null, "message");
+
+ // Add the failed test to the list of failed tests.
+ List errors = error != null ? List.of(error) : List.of();
+ failedTests.add(new LocalCIBuildResult.LocalCITestJobDTO(name, errors));
+ }
+ else {
+ // Add the successful test to the list of successful tests.
+ successfulTests.add(new LocalCIBuildResult.LocalCITestJobDTO(name, List.of()));
+ }
+ }
+
+ /**
+ * Constructs a {@link LocalCIBuildResult} that indicates a failed build from the given parameters. The lists of failed and successful tests are both empty which will be
+ * interpreted as a failed build by Artemis.
+ *
+ * @param assignmentRepoBranchName The name of the branch of the assignment repository that was checked out for the build.
+ * @param assignmentRepoCommitHash The commit hash of the assignment repository that was checked out for the build.
+ * @param testsRepoCommitHash The commit hash of the tests repository that was checked out for the build.
+ * @param buildRunDate The date when the build was completed.
+ * @return a {@link LocalCIBuildResult} that indicates a failed build
+ */
+ private LocalCIBuildResult constructFailedBuildResult(String assignmentRepoBranchName, String assignmentRepoCommitHash, String testsRepoCommitHash,
+ ZonedDateTime buildRunDate) {
+ return constructBuildResult(List.of(), List.of(), assignmentRepoBranchName, assignmentRepoCommitHash, testsRepoCommitHash, false, buildRunDate);
+ }
+
+ /**
+ * Constructs a {@link LocalCIBuildResult} from the given parameters.
+ *
+ * @param failedTests The list of failed tests.
+ * @param successfulTests The list of successful tests.
+ * @param assignmentRepoBranchName The name of the branch of the assignment repository that was checked out for the build.
+ * @param assignmentRepoCommitHash The commit hash of the assignment repository that was checked out for the build.
+ * @param testsRepoCommitHash The commit hash of the tests repository that was checked out for the build.
+ * @param isBuildSuccessful Whether the build was successful or not.
+ * @param buildRunDate The date when the build was completed.
+ * @return a {@link LocalCIBuildResult}
+ */
+ private LocalCIBuildResult constructBuildResult(List failedTests, List successfulTests,
+ String assignmentRepoBranchName, String assignmentRepoCommitHash, String testsRepoCommitHash, boolean isBuildSuccessful, ZonedDateTime buildRunDate) {
+ LocalCIBuildResult.LocalCIJobDTO job = new LocalCIBuildResult.LocalCIJobDTO(failedTests, successfulTests);
+
+ return new LocalCIBuildResult(assignmentRepoBranchName, assignmentRepoCommitHash, testsRepoCommitHash, isBuildSuccessful, buildRunDate, List.of(job));
+ }
+}
diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIBuildJobManagementService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIBuildJobManagementService.java
new file mode 100644
index 000000000000..24e208c14e4a
--- /dev/null
+++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIBuildJobManagementService.java
@@ -0,0 +1,209 @@
+package de.tum.in.www1.artemis.service.connectors.localci;
+
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Supplier;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Service;
+
+import de.tum.in.www1.artemis.domain.enumeration.ProjectType;
+import de.tum.in.www1.artemis.domain.participation.Participation;
+import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation;
+import de.tum.in.www1.artemis.exception.LocalCIException;
+import de.tum.in.www1.artemis.service.connectors.ci.ContinuousIntegrationService;
+import de.tum.in.www1.artemis.service.connectors.localci.dto.LocalCIBuildResult;
+import de.tum.in.www1.artemis.service.programming.ProgrammingMessagingService;
+import de.tum.in.www1.artemis.web.websocket.programmingSubmission.BuildTriggerWebsocketError;
+
+/**
+ * This service is responsible for adding build jobs to the local CI executor service.
+ * It handles timeouts as well as exceptions that occur during the execution of the build job.
+ */
+@Service
+@Profile("localci")
+public class LocalCIBuildJobManagementService {
+
+ private final Logger log = LoggerFactory.getLogger(LocalCIBuildJobManagementService.class);
+
+ private final LocalCIBuildJobExecutionService localCIBuildJobExecutionService;
+
+ private final ExecutorService localCIBuildExecutorService;
+
+ private final ProgrammingMessagingService programmingMessagingService;
+
+ private final LocalCIBuildPlanService localCIBuildPlanService;
+
+ private final LocalCIContainerService localCIContainerService;
+
+ @Value("${artemis.continuous-integration.timeout-seconds:120}")
+ private int timeoutSeconds;
+
+ @Value("${artemis.continuous-integration.asynchronous:true}")
+ private boolean runBuildJobsAsynchronously;
+
+ public LocalCIBuildJobManagementService(LocalCIBuildJobExecutionService localCIBuildJobExecutionService, ExecutorService localCIBuildExecutorService,
+ ProgrammingMessagingService programmingMessagingService, LocalCIBuildPlanService localCIBuildPlanService, LocalCIContainerService localCIContainerService) {
+ this.localCIBuildJobExecutionService = localCIBuildJobExecutionService;
+ this.localCIBuildExecutorService = localCIBuildExecutorService;
+ this.programmingMessagingService = programmingMessagingService;
+ this.localCIBuildPlanService = localCIBuildPlanService;
+ this.localCIContainerService = localCIContainerService;
+ }
+
+ /**
+ * Submit a build job for a given participation to the executor service.
+ *
+ * @param participation The participation of the repository for which the build job should be executed.
+ * @param commitHash The commit hash of the submission that led to this build. If it is "null", the latest commit of the repository will be used.
+ * @return A future that will be completed with the build result.
+ * @throws LocalCIException If the build job could not be submitted to the executor service.
+ */
+ public CompletableFuture addBuildJobToQueue(ProgrammingExerciseParticipation participation, String commitHash) {
+
+ // It should not be possible to create a programming exercise with a different project type than Gradle. This is just a sanity check.
+ ProjectType projectType = participation.getProgrammingExercise().getProjectType();
+ if (projectType == null || !projectType.isGradle()) {
+ throw new LocalCIException("Project type must be Gradle.");
+ }
+
+ // Prepare the Docker container name before submitting the build job to the executor service, so we can remove the container if something goes wrong.
+ String containerName = "artemis-local-ci-" + participation.getId() + "-" + ZonedDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"));
+
+ // Prepare a Callable that will later be called. It contains the actual steps needed to execute the build job.
+ Callable buildJob = () -> localCIBuildJobExecutionService.runBuildJob(participation, commitHash, containerName);
+
+ // Wrap the buildJob Callable in a BuildJobTimeoutCallable, so that the build job is cancelled if it takes too long.
+ BuildJobTimeoutCallable timedBuildJob = new BuildJobTimeoutCallable<>(buildJob, timeoutSeconds);
+
+ /*
+ * Submit the build job to the executor service. This runs in a separate thread, so it does not block the main thread.
+ * createCompletableFuture() is only used to provide a way to run build jobs synchronously for testing and debugging purposes and depends on the
+ * artemis.continuous-integration.asynchronous environment variable.
+ * Usually, when using asynchronous build jobs, it will just resolve to "CompletableFuture.supplyAsync".
+ */
+ CompletableFuture futureResult = createCompletableFuture(() -> {
+ try {
+ return localCIBuildExecutorService.submit(timedBuildJob).get();
+ }
+ catch (RejectedExecutionException | CancellationException | ExecutionException | InterruptedException e) {
+ // RejectedExecutionException is thrown if the queue size limit (defined in "artemis.continuous-integration.queue-size-limit") is reached.
+ finishBuildJobExceptionally(participation, commitHash, containerName, e);
+ // Wrap the exception in a CompletionException so that the future is completed exceptionally and the thenAccept block is not run.
+ // This CompletionException will not resurface anywhere else as it is thrown in this completable future's separate thread.
+ throw new CompletionException(e);
+ }
+ });
+
+ // Update the build plan status to "QUEUED".
+ localCIBuildPlanService.updateBuildPlanStatus(participation, ContinuousIntegrationService.BuildStatus.QUEUED);
+
+ return futureResult;
+ }
+
+ /**
+ * Create an asynchronous or a synchronous CompletableFuture depending on the runBuildJobsAsynchronously flag.
+ *
+ * @param supplier the supplier of the Future, i.e. the function that submits the build job
+ * @return the CompletableFuture
+ */
+ private CompletableFuture createCompletableFuture(Supplier supplier) {
+ if (runBuildJobsAsynchronously) {
+ // Just use the normal supplyAsync.
+ return CompletableFuture.supplyAsync(supplier);
+ }
+ else {
+ // Use a synchronous CompletableFuture, e.g. in the test environment.
+ // Otherwise, tests will not wait for the CompletableFuture to complete before asserting on the database.
+ CompletableFuture