diff --git a/docs/user/exams/student/access_exam.png b/docs/user/exams/student/access_exam.png index 76a0dddd02f3..a3579e5fc874 100644 Binary files a/docs/user/exams/student/access_exam.png and b/docs/user/exams/student/access_exam.png differ diff --git a/docs/user/exams/student/buttons/exam_hand_in_early.png b/docs/user/exams/student/buttons/exam_hand_in_early.png index d443c4116554..266d30608fbf 100644 Binary files a/docs/user/exams/student/buttons/exam_hand_in_early.png and b/docs/user/exams/student/buttons/exam_hand_in_early.png differ diff --git a/docs/user/exams/student/buttons/save_exercise.png b/docs/user/exams/student/buttons/save_exercise.png new file mode 100644 index 000000000000..7e16a5f622b5 Binary files /dev/null and b/docs/user/exams/student/buttons/save_exercise.png differ diff --git a/docs/user/exams/student/buttons/upload.png b/docs/user/exams/student/buttons/upload.png new file mode 100644 index 000000000000..cbe7b44079f7 Binary files /dev/null and b/docs/user/exams/student/buttons/upload.png differ diff --git a/docs/user/exams/student/exam_bar.png b/docs/user/exams/student/exam_bar.png index d1a4728bcc57..02914123a784 100644 Binary files a/docs/user/exams/student/exam_bar.png and b/docs/user/exams/student/exam_bar.png differ diff --git a/docs/user/exams/student/exam_modeling_exercises.png b/docs/user/exams/student/exam_modeling_exercises.png index 037cb07283aa..8fdd0912935e 100644 Binary files a/docs/user/exams/student/exam_modeling_exercises.png and b/docs/user/exams/student/exam_modeling_exercises.png differ diff --git a/docs/user/exams/student/exam_navigation_sidebar.png b/docs/user/exams/student/exam_navigation_sidebar.png index e8455e426df5..f6a45add9523 100644 Binary files a/docs/user/exams/student/exam_navigation_sidebar.png and b/docs/user/exams/student/exam_navigation_sidebar.png differ diff --git a/docs/user/exams/student/exam_overview.png b/docs/user/exams/student/exam_overview.png index eab66ecf60a7..84a85532187c 100644 Binary files a/docs/user/exams/student/exam_overview.png and b/docs/user/exams/student/exam_overview.png differ diff --git a/docs/user/exams/student/exam_programming_exercises.png b/docs/user/exams/student/exam_programming_exercises.png index b2018403e57a..1956f4fd173d 100644 Binary files a/docs/user/exams/student/exam_programming_exercises.png and b/docs/user/exams/student/exam_programming_exercises.png differ diff --git a/docs/user/exams/student/exam_quiz_exercises.png b/docs/user/exams/student/exam_quiz_exercises.png index 32c8d3634610..0c4bed59d3f6 100644 Binary files a/docs/user/exams/student/exam_quiz_exercises.png and b/docs/user/exams/student/exam_quiz_exercises.png differ diff --git a/docs/user/exams/student/exam_text_exercises.png b/docs/user/exams/student/exam_text_exercises.png index f81591045eb6..d79126ed398f 100644 Binary files a/docs/user/exams/student/exam_text_exercises.png and b/docs/user/exams/student/exam_text_exercises.png differ diff --git a/docs/user/exams/students_guide.rst b/docs/user/exams/students_guide.rst index b38278b75d22..f56f5fa94894 100644 --- a/docs/user/exams/students_guide.rst +++ b/docs/user/exams/students_guide.rst @@ -105,6 +105,14 @@ Welcome Screen Welcome Screen, waiting for exam start +This video offers a detailed guide on accessing your exams: + +.. raw:: html + + + Exam Conduction ^^^^^^^^^^^^^^^ - Once the exam working time starts and you have confirmed your participation, the *Exercise Overview* screen will appear. This screen lists all exercises that are part of your exam with their respective amount of points, title and exercise type. The status column indicates the status of each exercise and whether you have a submission in them or not. @@ -132,6 +140,14 @@ Exam Conduction Exam Navigation Sidebar +- You have two options to save your changes for an exercise: + + 1. Click the |save_exercise| button to manually save and submit your changes. + 2. Select an exercise in the navigation sidebar (either the current one or a different exercise), which will automatically save and submit your changes. + + .. warning:: + The |save_exercise| button is only available for text, modeling, and quiz exercises. For file upload exercises, you need to manually click the |upload| button, and for programming exercises, you need to manually click the |submit| button to save and submit your changes. + - On the header, you will find the exam bar that includes the remaining time and the |exam_hand_in_early| button. If you click this button, you will be sent to the exam `End Screen`_. - The *time left* until the end of the exam is also shown next to the button. @@ -373,6 +389,14 @@ Summary Complaining about the Assessment of a Text Exercise +This video offers a detailed guide on participating in your exams: + +.. raw:: html + + + Example Solutions ^^^^^^^^^^^^^^^^^ - If the instructor sets the example solution publication date of the exam, the solutions will be available after that date. @@ -470,3 +494,5 @@ Grades .. |exam_no_results_found| image:: student/buttons/exam_no_results_found.png .. |exam_hand_in_early| image:: student/buttons/exam_hand_in_early.png .. |saved_exercises| image:: student/buttons/saved_exercises.png +.. |upload| image:: student/buttons/upload.png +.. |save_exercise| image:: student/buttons/save_exercise.png diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CompetencyService.java b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CompetencyService.java index 705c162c6341..3b0a4bc083c9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CompetencyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CompetencyService.java @@ -51,8 +51,7 @@ public CompetencyService(CompetencyRepository competencyRepository, Authorizatio LearningObjectImportService learningObjectImportService, CompetencyLectureUnitLinkRepository competencyLectureUnitLinkRepository, CourseRepository courseRepository, CompetencyExerciseLinkRepository competencyExerciseLinkRepository) { super(competencyProgressRepository, courseCompetencyRepository, competencyRelationRepository, competencyProgressService, exerciseService, lectureUnitService, - learningPathService, authCheckService, standardizedCompetencyRepository, lectureUnitCompletionRepository, learningObjectImportService, - competencyLectureUnitLinkRepository, courseRepository); + learningPathService, authCheckService, standardizedCompetencyRepository, lectureUnitCompletionRepository, learningObjectImportService, courseRepository); this.competencyRepository = competencyRepository; this.competencyExerciseLinkRepository = competencyExerciseLinkRepository; } diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CourseCompetencyService.java b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CourseCompetencyService.java index cbe33e70b710..88cad15f1000 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CourseCompetencyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CourseCompetencyService.java @@ -30,7 +30,6 @@ import de.tum.cit.aet.artemis.atlas.dto.CompetencyRelationDTO; import de.tum.cit.aet.artemis.atlas.dto.CompetencyWithTailRelationDTO; import de.tum.cit.aet.artemis.atlas.dto.UpdateCourseCompetencyRelationDTO; -import de.tum.cit.aet.artemis.atlas.repository.CompetencyLectureUnitLinkRepository; import de.tum.cit.aet.artemis.atlas.repository.CompetencyProgressRepository; import de.tum.cit.aet.artemis.atlas.repository.CompetencyRelationRepository; import de.tum.cit.aet.artemis.atlas.repository.CourseCompetencyRepository; @@ -83,15 +82,13 @@ public class CourseCompetencyService { private final LearningObjectImportService learningObjectImportService; - private final CompetencyLectureUnitLinkRepository competencyLectureUnitLinkRepository; - private final CourseRepository courseRepository; public CourseCompetencyService(CompetencyProgressRepository competencyProgressRepository, CourseCompetencyRepository courseCompetencyRepository, CompetencyRelationRepository competencyRelationRepository, CompetencyProgressService competencyProgressService, ExerciseService exerciseService, LectureUnitService lectureUnitService, LearningPathService learningPathService, AuthorizationCheckService authCheckService, StandardizedCompetencyRepository standardizedCompetencyRepository, LectureUnitCompletionRepository lectureUnitCompletionRepository, - LearningObjectImportService learningObjectImportService, CompetencyLectureUnitLinkRepository competencyLectureUnitLinkRepository, CourseRepository courseRepository) { + LearningObjectImportService learningObjectImportService, CourseRepository courseRepository) { this.competencyProgressRepository = competencyProgressRepository; this.courseCompetencyRepository = courseCompetencyRepository; this.competencyRelationRepository = competencyRelationRepository; @@ -103,7 +100,6 @@ public CourseCompetencyService(CompetencyProgressRepository competencyProgressRe this.standardizedCompetencyRepository = standardizedCompetencyRepository; this.lectureUnitCompletionRepository = lectureUnitCompletionRepository; this.learningObjectImportService = learningObjectImportService; - this.competencyLectureUnitLinkRepository = competencyLectureUnitLinkRepository; this.courseRepository = courseRepository; } diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/PrerequisiteService.java b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/PrerequisiteService.java index eb66a98d641f..96a68280f334 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/PrerequisiteService.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/PrerequisiteService.java @@ -43,8 +43,7 @@ public PrerequisiteService(PrerequisiteRepository prerequisiteRepository, Author StandardizedCompetencyRepository standardizedCompetencyRepository, CourseCompetencyRepository courseCompetencyRepository, ExerciseService exerciseService, LearningObjectImportService learningObjectImportService, CompetencyLectureUnitLinkRepository competencyLectureUnitLinkRepository, CourseRepository courseRepository) { super(competencyProgressRepository, courseCompetencyRepository, competencyRelationRepository, competencyProgressService, exerciseService, lectureUnitService, - learningPathService, authCheckService, standardizedCompetencyRepository, lectureUnitCompletionRepository, learningObjectImportService, - competencyLectureUnitLinkRepository, courseRepository); + learningPathService, authCheckService, standardizedCompetencyRepository, lectureUnitCompletionRepository, learningObjectImportService, courseRepository); this.prerequisiteRepository = prerequisiteRepository; } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/repository/ConversationMessageRepository.java b/src/main/java/de/tum/cit/aet/artemis/communication/repository/ConversationMessageRepository.java index 21b3dfac0b81..2952c5213432 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/repository/ConversationMessageRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/repository/ConversationMessageRepository.java @@ -137,21 +137,4 @@ default Post findMessagePostByIdElseThrow(Long postId) throws EntityNotFoundExce WHERE p.id = :postId AND answer.author = cp.user """) Set findUsersWhoRepliedInMessage(@Param("postId") Long postId); - - /** - * Finds tags of course-wide messages - * - * @param courseId the course - * @return list of tags - */ - // TODO: unused, delete - @Query(""" - SELECT DISTINCT tag - FROM Post post - LEFT JOIN post.tags tag - LEFT JOIN Channel channel ON channel.id = post.conversation.id - WHERE channel.course.id = :courseId - AND channel.isCourseWide = TRUE - """) - List findPostTagsForCourse(@Param("courseId") Long courseId); } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/ConversationMessagingService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/ConversationMessagingService.java index 0020deca7faf..a54058431b76 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/ConversationMessagingService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/ConversationMessagingService.java @@ -3,8 +3,6 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import java.time.ZonedDateTime; -import java.util.Comparator; -import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -24,8 +22,6 @@ import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; -import com.google.common.collect.Lists; - import de.tum.cit.aet.artemis.communication.domain.ConversationNotificationRecipientSummary; import de.tum.cit.aet.artemis.communication.domain.CreatedConversationMessage; import de.tum.cit.aet.artemis.communication.domain.DisplayPriority; @@ -49,7 +45,6 @@ import de.tum.cit.aet.artemis.communication.service.conversation.auth.ChannelAuthorizationService; import de.tum.cit.aet.artemis.communication.service.notifications.ConversationNotificationService; import de.tum.cit.aet.artemis.communication.service.notifications.GroupNotificationService; -import de.tum.cit.aet.artemis.communication.service.similarity.PostSimilarityComparisonStrategy; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException; @@ -66,8 +61,6 @@ @Service public class ConversationMessagingService extends PostingService { - private static final int TOP_K_SIMILARITY_RESULTS = 5; - private static final Logger log = LoggerFactory.getLogger(ConversationMessagingService.class); private final ConversationService conversationService; @@ -82,14 +75,11 @@ public class ConversationMessagingService extends PostingService { private final SingleUserNotificationRepository singleUserNotificationRepository; - private final PostSimilarityComparisonStrategy postContentCompareStrategy; - protected ConversationMessagingService(CourseRepository courseRepository, ExerciseRepository exerciseRepository, LectureRepository lectureRepository, ConversationMessageRepository conversationMessageRepository, AuthorizationCheckService authorizationCheckService, WebsocketMessagingService websocketMessagingService, UserRepository userRepository, ConversationService conversationService, ConversationParticipantRepository conversationParticipantRepository, ConversationNotificationService conversationNotificationService, ChannelAuthorizationService channelAuthorizationService, - GroupNotificationService groupNotificationService, SingleUserNotificationRepository singleUserNotificationRepository, - PostSimilarityComparisonStrategy postContentCompareStrategy) { + GroupNotificationService groupNotificationService, SingleUserNotificationRepository singleUserNotificationRepository) { super(courseRepository, userRepository, exerciseRepository, lectureRepository, authorizationCheckService, websocketMessagingService, conversationParticipantRepository); this.conversationService = conversationService; this.conversationMessageRepository = conversationMessageRepository; @@ -97,7 +87,6 @@ protected ConversationMessagingService(CourseRepository courseRepository, Exerci this.channelAuthorizationService = channelAuthorizationService; this.groupNotificationService = groupNotificationService; this.singleUserNotificationRepository = singleUserNotificationRepository; - this.postContentCompareStrategy = postContentCompareStrategy; } /** @@ -433,41 +422,6 @@ private Conversation mayUpdateOrDeleteMessageElseThrow(Post existingMessagePost, } } - /** - * Calculates k similar posts based on the underlying content comparison strategy - * - * @param courseId id of the course in which similar posts are searched for - * @param post post that is to be created and check for similar posts beforehand - * @return list of similar posts - */ - // TODO: unused, remove - public List getSimilarPosts(Long courseId, Post post) { - PostContextFilterDTO postContextFilter = new PostContextFilterDTO(courseId, null, null, null, null, false, false, false, null, null); - List coursePosts = this.getCourseWideMessages(Pageable.unpaged(), postContextFilter, userRepository.getUser(), courseId).stream() - .sorted(Comparator.comparing(coursePost -> postContentCompareStrategy.performSimilarityCheck(post, coursePost))).toList(); - - // sort course posts by calculated similarity scores - setAuthorRoleOfPostings(coursePosts, courseId); - return Lists.reverse(coursePosts).stream().limit(TOP_K_SIMILARITY_RESULTS).toList(); - } - - /** - * Checks course and user validity, - * retrieves all tags for posts in a certain course - * - * @param courseId id of the course the tags belongs to - * @return tags of all posts that belong to the course - */ - // TODO: unused, delete - public List getAllCourseTags(Long courseId) { - final User user = userRepository.getUserWithGroupsAndAuthorities(); - final Course course = courseRepository.findByIdElseThrow(courseId); - - // checks - preCheckUserAndCourseForCommunicationOrMessaging(user, course); - return conversationMessageRepository.findPostTagsForCourse(courseId); - } - @Override public String getEntityName() { return METIS_POST_ENTITY_NAME; diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/similarity/PostSimilarityComparisonStrategy.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/similarity/PostSimilarityComparisonStrategy.java deleted file mode 100644 index 93ca9de0c9f9..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/similarity/PostSimilarityComparisonStrategy.java +++ /dev/null @@ -1,19 +0,0 @@ -package de.tum.cit.aet.artemis.communication.service.similarity; - -import de.tum.cit.aet.artemis.communication.domain.Post; - -/** - * This interface offers a method that performs a similarity check on two posts that are compared to each other. - * Every strategy that implements this interface has to provide this method in order to be applicable as post similarity comparison strategy, that can be interchanged easily. - */ -public interface PostSimilarityComparisonStrategy { - - /** - * Method implemented by every strategy; compares two posts using any suitable algorithm to determine similarity - * - * @param post1 first post object that is compared against - * @param post2 second post object that is compared against - * @return the calculated similarity score - */ - Double performSimilarityCheck(Post post1, Post post2); -} diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/similarity/TitleJaccardSimilarityCompareStrategy.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/similarity/TitleJaccardSimilarityCompareStrategy.java deleted file mode 100644 index 53e7285cd41b..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/similarity/TitleJaccardSimilarityCompareStrategy.java +++ /dev/null @@ -1,35 +0,0 @@ -package de.tum.cit.aet.artemis.communication.service.similarity; - -import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; - -import org.apache.commons.text.similarity.JaccardSimilarity; -import org.springframework.context.annotation.Primary; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -import de.tum.cit.aet.artemis.communication.domain.Post; - -/** - * Implementation of a PostSimilarityComparisonStrategy to be used when searching for duplicates during post creation. - * Jaccard Similarity is a common proximity measurement used to compute the similarity between two objects, such as two text documents; - * In the context of post comparison, the TitleJaccardSimilarityCompareStrategy determines the similarity between two titles (i.e. document) using the number of terms used in both - * documents. - * We use the JaccardSimilarity implementation provided by the org.apache.commons.text.similarity package. - */ -@Profile(PROFILE_CORE) -@Primary -@Component -public class TitleJaccardSimilarityCompareStrategy implements PostSimilarityComparisonStrategy { - - @Override - public Double performSimilarityCheck(Post post1, Post post2) { - JaccardSimilarity jaccardSimilarity = new JaccardSimilarity(); - Double similarityScore = 0.0; - - // we only compute a similarity score if the title of both posts are defined - if (post1.getTitle() != null && post2.getTitle() != null) { - similarityScore = jaccardSimilarity.apply(post1.getTitle().toLowerCase(), post2.getTitle().toLowerCase()); - } - return similarityScore; - } -} diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/web/ConversationMessageResource.java b/src/main/java/de/tum/cit/aet/artemis/communication/web/ConversationMessageResource.java index 5031332a8862..bfa04d53cc5f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/web/ConversationMessageResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/web/ConversationMessageResource.java @@ -201,34 +201,4 @@ public ResponseEntity updateDisplayPriority(@PathVariable Long courseId, @ Post postWithUpdatedDisplayPriority = conversationMessagingService.changeDisplayPriority(courseId, postId, displayPriority); return ResponseEntity.ok().body(postWithUpdatedDisplayPriority); } - - /** - * POST /courses/{courseId}/messages/similarity-check : trigger a similarity check for post to be created - * - * @param courseId id of the course the post should be published in - * @param post post to create - * @return ResponseEntity with status 200 (OK) - */ - @PostMapping("courses/{courseId}/messages/similarity-check") - @EnforceAtLeastStudent - // TODO: unused, remove - public ResponseEntity> computeSimilarityScoresWitCoursePosts(@PathVariable Long courseId, @RequestBody Post post) { - List similarPosts = conversationMessagingService.getSimilarPosts(courseId, post); - return ResponseEntity.ok().body(similarPosts); - } - - /** - * GET /courses/{courseId}/posts/tags : Get all tags for posts in a certain course - * - * @param courseId id of the course the post belongs to - * @return the ResponseEntity with status 200 (OK) and with body all tags for posts in that course, - * or 400 (Bad Request) if the checks on user or course validity fail - */ - @GetMapping("courses/{courseId}/messages/tags") - // TODO: unused, delete - @EnforceAtLeastStudent - public ResponseEntity> getAllPostTagsForCourse(@PathVariable Long courseId) { - List tags = conversationMessagingService.getAllCourseTags(courseId); - return new ResponseEntity<>(tags, null, HttpStatus.OK); - } } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/cleanup/DataCleanupService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/cleanup/DataCleanupService.java index a89a6f6ef207..2ed93c8d8926 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/cleanup/DataCleanupService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/cleanup/DataCleanupService.java @@ -13,7 +13,6 @@ import de.tum.cit.aet.artemis.assessment.repository.cleanup.FeedbackCleanupRepository; import de.tum.cit.aet.artemis.assessment.repository.cleanup.LongFeedbackTextCleanupRepository; -import de.tum.cit.aet.artemis.assessment.repository.cleanup.ParticipantScoreCleanupRepository; import de.tum.cit.aet.artemis.assessment.repository.cleanup.PlagiarismComparisonCleanupRepository; import de.tum.cit.aet.artemis.assessment.repository.cleanup.RatingCleanupRepository; import de.tum.cit.aet.artemis.assessment.repository.cleanup.ResultCleanupRepository; @@ -49,13 +48,10 @@ public class DataCleanupService { private final TeamScoreCleanupRepository teamScoreCleanupRepository; - private final ParticipantScoreCleanupRepository participantScoreCleanupRepository; - public DataCleanupService(CleanupJobExecutionRepository cleanupJobExecutionRepository, PlagiarismComparisonCleanupRepository plagiarismComparisonCleanupRepository, ResultCleanupRepository resultCleanupRepository, RatingCleanupRepository ratingCleanupRepository, FeedbackCleanupRepository feedbackCleanupRepository, TextBlockCleanupRepository textBlockCleanupRepository, LongFeedbackTextCleanupRepository longFeedbackTextCleanupRepository, - StudentScoreCleanupRepository studentScoreCleanupRepository, TeamScoreCleanupRepository teamScoreCleanupRepository, - ParticipantScoreCleanupRepository participantScoreCleanupRepository) { + StudentScoreCleanupRepository studentScoreCleanupRepository, TeamScoreCleanupRepository teamScoreCleanupRepository) { this.resultCleanupRepository = resultCleanupRepository; this.ratingCleanupRepository = ratingCleanupRepository; this.feedbackCleanupRepository = feedbackCleanupRepository; @@ -65,7 +61,6 @@ public DataCleanupService(CleanupJobExecutionRepository cleanupJobExecutionRepos this.teamScoreCleanupRepository = teamScoreCleanupRepository; this.cleanupJobExecutionRepository = cleanupJobExecutionRepository; this.plagiarismComparisonCleanupRepository = plagiarismComparisonCleanupRepository; - this.participantScoreCleanupRepository = participantScoreCleanupRepository; } // TODO: offer the possibility to delete old submission versions diff --git a/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamLiveEventsService.java b/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamLiveEventsService.java index fa48e0c1a09c..31ee3dbf9e80 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamLiveEventsService.java +++ b/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamLiveEventsService.java @@ -8,7 +8,6 @@ import de.tum.cit.aet.artemis.communication.service.WebsocketMessagingService; import de.tum.cit.aet.artemis.core.domain.User; -import de.tum.cit.aet.artemis.core.repository.UserRepository; import de.tum.cit.aet.artemis.exam.domain.Exam; import de.tum.cit.aet.artemis.exam.domain.StudentExam; import de.tum.cit.aet.artemis.exam.domain.event.ExamAttendanceCheckEvent; @@ -55,14 +54,11 @@ public class ExamLiveEventsService { private final StudentExamRepository studentExamRepository; - private final UserRepository userRepository; - - public ExamLiveEventsService(WebsocketMessagingService websocketMessagingService, ExamLiveEventRepository examLiveEventRepository, StudentExamRepository studentExamRepository, - UserRepository userRepository) { + public ExamLiveEventsService(WebsocketMessagingService websocketMessagingService, ExamLiveEventRepository examLiveEventRepository, + StudentExamRepository studentExamRepository) { this.websocketMessagingService = websocketMessagingService; this.examLiveEventRepository = examLiveEventRepository; this.studentExamRepository = studentExamRepository; - this.userRepository = userRepository; } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/repository/StudentParticipationRepository.java b/src/main/java/de/tum/cit/aet/artemis/exercise/repository/StudentParticipationRepository.java index 1101f94d4708..bb775298efb5 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/repository/StudentParticipationRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/repository/StudentParticipationRepository.java @@ -59,7 +59,8 @@ public interface StudentParticipationRepository extends ArtemisJpaRepository findByCourseIdWithEagerRatedResults(@Param("courseId") long courseId); @@ -69,8 +70,10 @@ public interface StudentParticipationRepository extends ArtemisJpaRepository findByCourseIdAndStudentIdWithEagerRatedResults(@Param("courseId") long courseId, @Param("studentId") long studentId); @@ -79,7 +82,8 @@ SELECT COUNT(p.id) > 0 FROM StudentParticipation p LEFT JOIN p.team.students ts WHERE p.exercise.course.id = :courseId - AND (p.student.id = :studentId OR ts.id = :studentId) + AND (p.student.id = :studentId + OR ts.id = :studentId) """) boolean existsByCourseIdAndStudentId(@Param("courseId") long courseId, @Param("studentId") long studentId); @@ -91,7 +95,8 @@ SELECT COUNT(p.id) > 0 WHERE p.testRun = FALSE AND p.exercise.exerciseGroup.exam.id = :examId AND r.rated = TRUE - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) """) List findByExamIdWithEagerLegalSubmissionsRatedResults(@Param("examId") long examId); @@ -125,7 +130,8 @@ SELECT COUNT(p.id) > 0 LEFT JOIN FETCH p.submissions s WHERE p.exercise.id = :exerciseId AND p.student.login = :username - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) """) Optional findWithEagerLegalSubmissionsByExerciseIdAndStudentLogin(@Param("exerciseId") long exerciseId, @Param("username") String username); @@ -135,7 +141,8 @@ SELECT COUNT(p.id) > 0 LEFT JOIN FETCH p.submissions s WHERE p.exercise.id = :exerciseId AND p.student.login = :username - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) AND p.testRun = :testRun """) Optional findWithEagerLegalSubmissionsByExerciseIdAndStudentLoginAndTestRun(@Param("exerciseId") long exerciseId, @Param("username") String username, @@ -157,7 +164,8 @@ Optional findWithEagerLegalSubmissionsByExerciseIdAndStude LEFT JOIN FETCH t.students WHERE p.exercise.id = :exerciseId AND p.team.id = :teamId - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) """) Optional findWithEagerLegalSubmissionsAndTeamStudentsByExerciseIdAndTeamId(@Param("exerciseId") long exerciseId, @Param("teamId") long teamId); @@ -175,8 +183,9 @@ SELECT COUNT(p) > 0 FROM StudentParticipation p LEFT JOIN p.team.students u LEFT JOIN p.student s - WHERE p.id = :participationId AND - (s.login = :login OR u.login = :login) + WHERE p.id = :participationId + AND (s.login = :login + OR u.login = :login) """) boolean existsByIdAndParticipatingStudentLogin(@Param("participationId") long participationId, @Param("login") String login); @@ -187,7 +196,8 @@ SELECT COUNT(p) > 0 LEFT JOIN FETCH s.results WHERE p.exercise.id = :exerciseId AND p.testRun = :testRun - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) """) List findByExerciseIdAndTestRunWithEagerLegalSubmissionsResult(@Param("exerciseId") long exerciseId, @Param("testRun") boolean testRun); @@ -235,7 +245,10 @@ Optional findByExerciseIdAndStudentIdAndTestRunWithEagerSu LEFT JOIN FETCH r.assessmentNote WHERE p.exercise.id = :exerciseId AND ( - r.id = (SELECT MAX(p_r.id) FROM p.results p_r) + r.id = ( + SELECT MAX(p_r.id) + FROM p.results p_r + ) OR r.assessmentType <> de.tum.cit.aet.artemis.assessment.domain.AssessmentType.AUTOMATIC OR r IS NULL ) @@ -260,29 +273,16 @@ Optional findByExerciseIdAndStudentIdAndTestRunWithEagerSu LEFT JOIN FETCH t.students WHERE p.exercise.id = :exerciseId AND ( - r.id = (SELECT MAX(p_r.id) FROM p.results p_r) + r.id = ( + SELECT MAX(p_r.id) + FROM p.results p_r + ) OR r.assessmentType <> de.tum.cit.aet.artemis.assessment.domain.AssessmentType.AUTOMATIC OR r IS NULL ) """) Set findByExerciseIdWithLatestAndManualResultsWithTeamInformation(@Param("exerciseId") long exerciseId); - @Query(""" - SELECT DISTINCT p - FROM StudentParticipation p - LEFT JOIN FETCH p.results r - LEFT JOIN FETCH r.submission s - LEFT JOIN FETCH p.submissions - LEFT JOIN FETCH r.assessmentNote - WHERE p.exercise.id = :exerciseId - AND ( - r.id = (SELECT MAX(p_r.id) FROM p.results p_r WHERE p_r.rated = TRUE) - OR r.assessmentType <> de.tum.cit.aet.artemis.assessment.domain.AssessmentType.AUTOMATIC - OR r IS NULL - ) - """) - Set findByExerciseIdWithLatestAndManualRatedResultsAndAssessmentNote(@Param("exerciseId") long exerciseId); - @Query(""" SELECT DISTINCT p FROM StudentParticipation p @@ -292,7 +292,11 @@ Optional findByExerciseIdAndStudentIdAndTestRunWithEagerSu AND p.testRun = :testRun AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) AND r.assessmentType <> de.tum.cit.aet.artemis.assessment.domain.AssessmentType.AUTOMATIC - AND r.id = (SELECT MAX(r2.id) FROM p.results r2 WHERE r2.completionDate IS NOT NULL) + AND r.id = ( + SELECT MAX(r2.id) + FROM p.results r2 + WHERE r2.completionDate IS NOT NULL + ) """) Set findByExerciseIdAndTestRunWithEagerLegalSubmissionsAndLatestResultWithCompletionDate(@Param("exerciseId") long exerciseId, @Param("testRun") boolean testRun); @@ -343,16 +347,14 @@ default List findByExerciseIdWithLatestAutomaticResultAndF LEFT JOIN FETCH f.testCase LEFT JOIN FETCH r.submission s WHERE p.id = :participationId - AND (r.id = ( + AND r.id = ( SELECT MAX(pr.id) FROM p.results pr LEFT JOIN pr.submission prs WHERE pr.assessmentType = de.tum.cit.aet.artemis.assessment.domain.AssessmentType.AUTOMATIC - AND ( - prs.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL - OR prs.type IS NULL - ) - )) + AND (prs.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR prs.type IS NULL) + ) """) Optional findByIdWithLatestAutomaticResultAndFeedbacksAndTestCases(@Param("participationId") long participationId); @@ -366,10 +368,8 @@ SELECT MAX(pr.id) LEFT JOIN FETCH r.submission s WHERE p.exercise.id = :exerciseId AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) - AND ( - r.assessmentType = de.tum.cit.aet.artemis.assessment.domain.AssessmentType.MANUAL - OR r.assessmentType = de.tum.cit.aet.artemis.assessment.domain.AssessmentType.SEMI_AUTOMATIC - ) + AND (r.assessmentType = de.tum.cit.aet.artemis.assessment.domain.AssessmentType.MANUAL + OR r.assessmentType = de.tum.cit.aet.artemis.assessment.domain.AssessmentType.SEMI_AUTOMATIC) """) List findByExerciseIdWithManualResultAndFeedbacksAndTestCases(@Param("exerciseId") long exerciseId); @@ -385,11 +385,10 @@ default List findByExerciseIdWithManualResultAndFeedbacksA LEFT JOIN FETCH f.testCase LEFT JOIN FETCH r.submission s WHERE p.id = :participationId - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) - AND ( - r.assessmentType = de.tum.cit.aet.artemis.assessment.domain.AssessmentType.MANUAL - OR r.assessmentType = de.tum.cit.aet.artemis.assessment.domain.AssessmentType.SEMI_AUTOMATIC - ) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) + AND (r.assessmentType = de.tum.cit.aet.artemis.assessment.domain.AssessmentType.MANUAL + OR r.assessmentType = de.tum.cit.aet.artemis.assessment.domain.AssessmentType.SEMI_AUTOMATIC) """) Optional findByIdWithManualResultAndFeedbacks(@Param("participationId") long participationId); @@ -399,7 +398,8 @@ default List findByExerciseIdWithManualResultAndFeedbacksA LEFT JOIN FETCH p.submissions s WHERE p.exercise.id = :exerciseId AND p.student.id = :studentId - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) """) List findByExerciseIdAndStudentIdWithEagerLegalSubmissions(@Param("exerciseId") long exerciseId, @Param("studentId") long studentId); @@ -427,7 +427,8 @@ default List findByExerciseIdWithManualResultAndFeedbacksA LEFT JOIN FETCH p.submissions s WHERE p.exercise.id = :exerciseId AND p.team.id = :teamId - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) """) List findByExerciseIdAndTeamIdWithEagerLegalSubmissions(@Param("exerciseId") long exerciseId, @Param("teamId") long teamId); @@ -451,8 +452,10 @@ default List findByExerciseIdWithManualResultAndFeedbacksA LEFT JOIN FETCH t.students WHERE p.exercise.id = :exerciseId AND p.team.id = :teamId - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) - AND (rs.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR rs.type IS NULL) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) + AND (rs.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR rs.type IS NULL) """) List findByExerciseIdAndTeamIdWithEagerResultsAndLegalSubmissionsAndTeamStudents(@Param("exerciseId") long exerciseId, @Param("teamId") long teamId); @@ -471,7 +474,8 @@ SELECT MAX(pr.id) LEFT JOIN pr.submission prs WHERE prs.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR prs.type IS NULL - ) OR r.id IS NULL) + ) + OR r.id IS NULL) """) Optional findByExerciseIdAndStudentIdAndTestRunWithLatestResult(@Param("exerciseId") long exerciseId, @Param("studentId") long studentId, @Param("testRun") boolean testRun); @@ -524,7 +528,10 @@ WHERE prs.assessmentType IN ( ) ) AND submission.submitted = TRUE - AND submission.id = (SELECT MAX(s.id) FROM p.submissions s) + AND submission.id = ( + SELECT MAX(s.id) + FROM p.submissions s + ) """) List findByExerciseIdWithLatestSubmissionWithoutManualResultsAndIgnoreTestRunParticipation(@Param("exerciseId") long exerciseId, @Param("correctionRound") long correctionRound); @@ -548,7 +555,10 @@ WHERE prs.assessmentType IN ( de.tum.cit.aet.artemis.assessment.domain.AssessmentType.SEMI_AUTOMATIC ) ) AND s.submitted = TRUE - AND s.id = (SELECT MAX(s.id) FROM p.submissions s) + AND s.id = ( + SELECT MAX(s.id) + FROM p.submissions s + ) """) List findByExerciseIdWithLatestSubmissionWithoutManualResultsWithPassedIndividualDueDateIgnoreTestRuns(@Param("exerciseId") long exerciseId, @Param("now") ZonedDateTime now); @@ -558,7 +568,8 @@ List findByExerciseIdWithLatestSubmissionWithoutManualResu FROM Participation p LEFT JOIN FETCH p.submissions s WHERE p.id = :participationId - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) """) Optional findWithEagerLegalSubmissionsById(@Param("participationId") long participationId); @@ -593,8 +604,10 @@ List findByExerciseIdWithLatestSubmissionWithoutManualResu LEFT JOIN FETCH p.team t LEFT JOIN FETCH t.students WHERE p.id = :participationId - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) - AND (rs.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR rs.type IS NULL) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) + AND (rs.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR rs.type IS NULL) """) Optional findWithEagerLegalSubmissionsResultsFeedbacksById(@Param("participationId") long participationId); @@ -617,10 +630,9 @@ List findByExerciseIdWithLatestSubmissionWithoutManualResu FROM StudentParticipation p JOIN Result r ON r.participation.id = p.id WHERE p.exercise.id = :exerciseId - AND ( - p.student.firstName LIKE %:partialStudentName% - OR p.student.lastName LIKE %:partialStudentName% - ) AND r.completionDate IS NOT NULL + AND (p.student.firstName LIKE %:partialStudentName% + OR p.student.lastName LIKE %:partialStudentName%) + AND r.completionDate IS NOT NULL """) List findIdsByExerciseIdAndStudentName(@Param("exerciseId") long exerciseId, @Param("partialStudentName") String partialStudentName, Pageable pageable); @@ -632,10 +644,9 @@ SELECT COUNT(p) FROM StudentParticipation p JOIN Result r ON r.participation.id = p.id WHERE p.exercise.id = :exerciseId - AND ( - p.student.firstName LIKE %:partialStudentName% - OR p.student.lastName LIKE %:partialStudentName% - ) AND r.completionDate IS NOT NULL + AND (p.student.firstName LIKE %:partialStudentName% + OR p.student.lastName LIKE %:partialStudentName%) + AND r.completionDate IS NOT NULL """) long countByExerciseIdAndStudentName(@Param("exerciseId") long exerciseId, @Param("partialStudentName") String partialStudentName); @@ -666,8 +677,10 @@ default Page findAllWithEagerSubmissionsAndResultsByExerci LEFT JOIN FETCH p.submissions s LEFT JOIN FETCH s.results sr WHERE p.exercise.id = :exerciseId - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) - AND (rs.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR rs.type IS NULL) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) + AND (rs.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR rs.type IS NULL) """) List findAllWithEagerLegalSubmissionsAndEagerResultsByExerciseId(@Param("exerciseId") long exerciseId); @@ -697,12 +710,17 @@ default Page findAllWithEagerSubmissionsAndResultsByExerci LEFT JOIN FETCH p.team WHERE p.exercise.id = :exerciseId AND p.testRun = FALSE - AND s.id = (SELECT MAX(s2.id) - FROM p.submissions s2 - WHERE s2.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s2.type IS NULL) - AND r.id = (SELECT MAX(r2.id) - FROM s.results r2 - WHERE r2.rated = TRUE) + AND s.id = ( + SELECT MAX(s2.id) + FROM p.submissions s2 + WHERE s2.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s2.type IS NULL + ) + AND r.id = ( + SELECT MAX(r2.id) + FROM s.results r2 + WHERE r2.rated = TRUE + ) """) List findAllForPlagiarism(@Param("exerciseId") long exerciseId); @@ -713,7 +731,8 @@ default Page findAllWithEagerSubmissionsAndResultsByExerci LEFT JOIN FETCH s.results r WHERE p.student.id = :studentId AND p.exercise IN :exercises - AND (p.testRun = FALSE OR :includeTestRuns = TRUE) + AND (p.testRun = FALSE + OR :includeTestRuns = TRUE) """) Set findByStudentIdAndIndividualExercisesWithEagerSubmissionsResult(@Param("studentId") long studentId, @Param("exercises") Collection exercises, @Param("includeTestRuns") boolean includeTestRuns); @@ -786,7 +805,8 @@ List findTestRunParticipationsByStudentIdAndIndividualExer LEFT JOIN FETCH t.students teamStudent WHERE teamStudent.id = :studentId AND p.exercise IN :exercises - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) """) Set findByStudentIdAndTeamExercisesWithEagerLegalSubmissionsResult(@Param("studentId") long studentId, @Param("exercises") Collection exercises); @@ -799,7 +819,8 @@ Set findByStudentIdAndTeamExercisesWithEagerLegalSubmissio LEFT JOIN FETCH p.team t WHERE p.exercise.course.id = :courseId AND t.shortName = :teamShortName - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) """) List findAllByCourseIdAndTeamShortNameWithEagerLegalSubmissionsResult(@Param("courseId") long courseId, @Param("teamShortName") String teamShortName); @@ -831,7 +852,8 @@ SELECT p.id, COUNT(s) LEFT JOIN p.submissions s WHERE p.team.shortName = :teamShortName AND p.exercise.course.id = :courseId - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) GROUP BY p.id """) List countLegalSubmissionsPerParticipationByCourseIdAndTeamShortName(@Param("courseId") long courseId, @Param("teamShortName") String teamShortName); @@ -850,7 +872,8 @@ AND EXISTS ( FROM p.submissions s1 WHERE s1.participation.id = p.id AND s1.submitted = TRUE - AND (r.assessor = :assessor OR r.assessor.id IS NULL) + AND (r.assessor = :assessor + OR r.assessor.id IS NULL) ) """) List findAllByParticipationExerciseIdAndResultAssessorAndCorrectionRoundIgnoreTestRuns(@Param("exerciseId") long exerciseId, diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/service/ExerciseDeletionService.java b/src/main/java/de/tum/cit/aet/artemis/exercise/service/ExerciseDeletionService.java index 6739cfc0c714..36b7e99fedd0 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/service/ExerciseDeletionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/service/ExerciseDeletionService.java @@ -17,7 +17,6 @@ import de.tum.cit.aet.artemis.assessment.repository.TutorParticipationRepository; import de.tum.cit.aet.artemis.assessment.service.ExampleSubmissionService; import de.tum.cit.aet.artemis.atlas.domain.competency.CompetencyExerciseLink; -import de.tum.cit.aet.artemis.atlas.repository.CompetencyExerciseLinkRepository; import de.tum.cit.aet.artemis.atlas.service.competency.CompetencyProgressService; import de.tum.cit.aet.artemis.communication.domain.conversation.Channel; import de.tum.cit.aet.artemis.communication.repository.conversation.ChannelRepository; @@ -81,8 +80,6 @@ public class ExerciseDeletionService { private final CompetencyProgressService competencyProgressService; - private final CompetencyExerciseLinkRepository competencyExerciseLinkRepository; - private final Optional irisSettingsService; public ExerciseDeletionService(ExerciseRepository exerciseRepository, ExerciseUnitRepository exerciseUnitRepository, ParticipationService participationService, @@ -90,7 +87,7 @@ public ExerciseDeletionService(ExerciseRepository exerciseRepository, ExerciseUn TutorParticipationRepository tutorParticipationRepository, ExampleSubmissionService exampleSubmissionService, StudentExamRepository studentExamRepository, LectureUnitService lectureUnitService, PlagiarismResultRepository plagiarismResultRepository, TextExerciseService textExerciseService, ChannelRepository channelRepository, ChannelService channelService, CompetencyProgressService competencyProgressService, - CompetencyExerciseLinkRepository competencyExerciseLinkRepository, Optional irisSettingsService) { + Optional irisSettingsService) { this.exerciseRepository = exerciseRepository; this.participationService = participationService; this.programmingExerciseService = programmingExerciseService; @@ -106,7 +103,6 @@ public ExerciseDeletionService(ExerciseRepository exerciseRepository, ExerciseUn this.channelRepository = channelRepository; this.channelService = channelService; this.competencyProgressService = competencyProgressService; - this.competencyExerciseLinkRepository = competencyExerciseLinkRepository; this.irisSettingsService = irisSettingsService; } diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java b/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java index 4d549bb3fe66..05e3166ff171 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java @@ -66,7 +66,6 @@ import de.tum.cit.aet.artemis.core.service.messaging.InstanceMessageSendService; import de.tum.cit.aet.artemis.core.util.HeaderUtil; import de.tum.cit.aet.artemis.exercise.domain.Exercise; -import de.tum.cit.aet.artemis.exercise.domain.ExerciseType; import de.tum.cit.aet.artemis.exercise.domain.InitializationState; import de.tum.cit.aet.artemis.exercise.domain.Submission; import de.tum.cit.aet.artemis.exercise.domain.SubmissionType; @@ -595,9 +594,7 @@ public ResponseEntity> updateParticipationDueDates(@P } private Set findParticipationWithLatestResults(Exercise exercise) { - if (exercise.getExerciseType() == ExerciseType.QUIZ) { - return studentParticipationRepository.findByExerciseIdWithLatestAndManualRatedResultsAndAssessmentNote(exercise.getId()); - } + // TODO: we should reduce the amount of data fetched here and sent to the client: double check which data is actually required in the exercise scores page if (exercise.isTeamMode()) { // For team exercises the students need to be eagerly fetched return studentParticipationRepository.findByExerciseIdWithLatestAndManualResultsWithTeamInformation(exercise.getId()); diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisExerciseChatSessionResource.java b/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisExerciseChatSessionResource.java index 9e736ce8c358..3f2a0bdb6ab7 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisExerciseChatSessionResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisExerciseChatSessionResource.java @@ -101,7 +101,7 @@ public ResponseEntity getCurrentSessionOrCreateIfNotExi @EnforceAtLeastStudentInExercise public ResponseEntity> getAllSessions(@PathVariable Long exerciseId) { var exercise = exerciseRepository.findByIdElseThrow(exerciseId); - ProgrammingExercise programmingExercise = validateExercise(exercise); + validateExercise(exercise); irisSettingsService.isEnabledForElseThrow(IrisSubSettingsType.CHAT, exercise); var user = userRepository.getUserWithGroupsAndAuthorities(); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseStudentParticipationRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseStudentParticipationRepository.java index cc1f57c533fa..c88024f0835b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseStudentParticipationRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseStudentParticipationRepository.java @@ -136,8 +136,8 @@ Optional findWithSubmissionsAndEagerStu @EntityGraph(type = LOAD, attributePaths = { "submissions", "team.students" }) List findWithSubmissionsById(long participationId); - @EntityGraph(type = LOAD, attributePaths = { "submissions" }) - List findWithSubmissionsByExerciseId(long exerciseId); + @EntityGraph(type = LOAD, attributePaths = { "submissions.results" }) + List findWithSubmissionsAndResultsByExerciseId(long exerciseId); @EntityGraph(type = LOAD, attributePaths = { "submissions", "team.students" }) List findWithSubmissionsAndTeamStudentsByExerciseId(long exerciseId); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingSubmissionRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingSubmissionRepository.java index ffe438217b64..90d514ca4b68 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingSubmissionRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingSubmissionRepository.java @@ -50,7 +50,8 @@ default ProgrammingSubmission findFirstByParticipationIdAndCommitHashOrderByIdDe @Query(value = """ SELECT new de.tum.cit.aet.artemis.programming.dto.ProgrammingSubmissionIdAndSubmissionDateDTO(ps.id, ps.submissionDate) FROM ProgrammingSubmission ps - WHERE ps.participation.id = :participationId ORDER BY ps.submissionDate DESC + WHERE ps.participation.id = :participationId + ORDER BY ps.submissionDate DESC """) List findFirstIdByParticipationIdOrderBySubmissionDateDesc(@Param("participationId") long participationId, Pageable pageable); @@ -72,8 +73,8 @@ default Optional findFirstByParticipationIdWithResultsOrd if (result.isEmpty()) { return Optional.empty(); } - long id = result.getFirst().programmingSubmissionId(); - return findProgrammingSubmissionWithResultsById(id); + long submissionId = result.getFirst().programmingSubmissionId(); + return findProgrammingSubmissionWithResultsById(submissionId); } @Query(""" @@ -104,8 +105,7 @@ default Optional findFirstByParticipationIdWithResultsOrd * @return ProgrammingSubmission list (can be empty!) */ default List findGradedByParticipationIdWithResultsOrderBySubmissionDateDesc(long participationId, Pageable pageable) { - List ids = findSubmissionIdsAndDatesByParticipationId(participationId, pageable).stream().map(ProgrammingSubmissionIdAndSubmissionDateDTO::programmingSubmissionId) - .toList(); + var ids = findSubmissionIdsAndDatesByParticipationId(participationId, pageable).stream().map(ProgrammingSubmissionIdAndSubmissionDateDTO::programmingSubmissionId).toList(); if (ids.isEmpty()) { return Collections.emptyList(); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ParticipationVcsAccessTokenService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ParticipationVcsAccessTokenService.java index db9cebb2eb6c..bbcdc1d5624b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ParticipationVcsAccessTokenService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ParticipationVcsAccessTokenService.java @@ -9,7 +9,9 @@ import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException; +import de.tum.cit.aet.artemis.exercise.domain.Team; import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation; +import de.tum.cit.aet.artemis.exercise.repository.TeamRepository; import de.tum.cit.aet.artemis.programming.domain.ParticipationVCSAccessToken; import de.tum.cit.aet.artemis.programming.repository.ParticipationVCSAccessTokenRepository; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseStudentParticipationRepository; @@ -25,10 +27,13 @@ public class ParticipationVcsAccessTokenService { private final ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository; + private final TeamRepository teamRepository; + public ParticipationVcsAccessTokenService(ParticipationVCSAccessTokenRepository participationVCSAccessTokenRepository, - ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository) { + ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, TeamRepository teamRepository) { this.participationVcsAccessTokenRepository = participationVCSAccessTokenRepository; this.programmingExerciseStudentParticipationRepository = programmingExerciseStudentParticipationRepository; + this.teamRepository = teamRepository; } /** @@ -55,6 +60,7 @@ public ParticipationVCSAccessToken createParticipationVCSAccessToken(User user, */ public ParticipationVCSAccessToken findByUserAndParticipationIdOrElseThrow(User user, long participationId) { var participation = programmingExerciseStudentParticipationRepository.findByIdElseThrow(participationId); + loadTeamStudentsForTeamExercise(participation); if (participation.isOwnedBy(user)) { return participationVcsAccessTokenRepository.findByUserIdAndParticipationIdOrElseThrow(user.getId(), participationId); } @@ -73,6 +79,7 @@ public ParticipationVCSAccessToken findByUserAndParticipationIdOrElseThrow(User public ParticipationVCSAccessToken createVcsAccessTokenForUserAndParticipationIdOrElseThrow(User user, long participationId) { participationVcsAccessTokenRepository.findByUserIdAndParticipationIdAndThrowIfExists(user.getId(), participationId); var participation = programmingExerciseStudentParticipationRepository.findByIdElseThrow(participationId); + loadTeamStudentsForTeamExercise(participation); if (participation.isOwnedBy(user)) { return createParticipationVCSAccessToken(user, participation); } @@ -81,6 +88,19 @@ public ParticipationVCSAccessToken createVcsAccessTokenForUserAndParticipationId } } + /** + * Loads the team students of a participation's team, if it has a team + * + * @param participation the participation which team's students are not loaded yet + */ + private void loadTeamStudentsForTeamExercise(StudentParticipation participation) { + if (participation.getTeam().isPresent()) { + Team team = participation.getTeam().get(); + Team teamWithStudents = teamRepository.findWithStudentsByIdElseThrow(team.getId()); + participation.getTeam().get().setStudents(teamWithStudents.getStudents()); + } + } + /** * Deletes the token connected to a participation * diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingSubmissionService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingSubmissionService.java index 58665d8beae4..9de4485f16b2 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingSubmissionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingSubmissionService.java @@ -6,6 +6,7 @@ import java.time.ZonedDateTime; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -341,14 +342,16 @@ public Optional getLatestPendingSubmission(Long participa * @return a Map of {[participationId]: ProgrammingSubmission | null}. Will contain an entry for every student participation of the exercise and a submission object if a * pending submission exists or null if not. */ - public Map> getLatestPendingSubmissionsForProgrammingExercise(Long programmingExerciseId) { - List participations = programmingExerciseStudentParticipationRepository.findWithSubmissionsByExerciseId(programmingExerciseId); - // TODO: find the latest pending submission directly using Java (the submissions are available now) and not with additional db queries - return participations.stream().collect(Collectors.toMap(Participation::getId, p -> findLatestPendingSubmissionForParticipation(p.getId()))); - } - - private Optional findLatestPendingSubmissionForParticipation(final long participationId) { - return findLatestPendingSubmissionForParticipation(participationId, false); + public Map> getLatestPendingSubmissionsForProgrammingExercise(Long programmingExerciseId) { + var participations = programmingExerciseStudentParticipationRepository.findWithSubmissionsAndResultsByExerciseId(programmingExerciseId); + return participations.stream().collect(Collectors.toMap(Participation::getId, p -> { + var latestSubmission = p.getSubmissions().stream().max(Comparator.comparing(Submission::getSubmissionDate)); + if (latestSubmission.isEmpty() || latestSubmission.get().getLatestResult() != null) { + // This is not an error case, it is very likely that there is no pending submission for a participation. + return Optional.empty(); + } + return latestSubmission; + })); } private Optional findLatestPendingSubmissionForParticipation(final long participationId, final boolean isGraded) { diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseParticipationResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseParticipationResource.java index be1c99c67be6..2566e1a87fb8 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseParticipationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseParticipationResource.java @@ -37,6 +37,7 @@ import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; import de.tum.cit.aet.artemis.exam.repository.StudentExamRepository; import de.tum.cit.aet.artemis.exam.service.ExamService; +import de.tum.cit.aet.artemis.exercise.domain.Submission; import de.tum.cit.aet.artemis.exercise.domain.participation.Participation; import de.tum.cit.aet.artemis.exercise.repository.ParticipationRepository; import de.tum.cit.aet.artemis.exercise.service.ParticipationAuthorizationCheckService; @@ -230,17 +231,17 @@ public ResponseEntity getLatestPendingSubmission(@PathVar */ @GetMapping("programming-exercises/{exerciseId}/latest-pending-submissions") @EnforceAtLeastTutor - public ResponseEntity>> getLatestPendingSubmissionsByExerciseId(@PathVariable Long exerciseId) { - ProgrammingExercise programmingExercise; - programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationElseThrow(exerciseId); + public ResponseEntity>> getLatestPendingSubmissionsByExerciseId(@PathVariable Long exerciseId) { + ProgrammingExercise programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationElseThrow(exerciseId); if (!authCheckService.isAtLeastTeachingAssistantForExercise(programmingExercise)) { throw new AccessForbiddenException("exercise", exerciseId); } - Map> pendingSubmissions = submissionService.getLatestPendingSubmissionsForProgrammingExercise(exerciseId); + // TODO: use a different data structure than map here + Map> pendingSubmissions = submissionService.getLatestPendingSubmissionsForProgrammingExercise(exerciseId); // Remove unnecessary data to make response smaller (exercise, student of participation). pendingSubmissions = pendingSubmissions.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> { - Optional submissionOpt = entry.getValue(); + Optional submissionOpt = entry.getValue(); // Remove participation, is not needed in the response. submissionOpt.ifPresent(submission -> submission.setParticipation(null)); return submissionOpt; diff --git a/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizExerciseService.java b/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizExerciseService.java index 274718a9b382..955212be3450 100644 --- a/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizExerciseService.java +++ b/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizExerciseService.java @@ -33,7 +33,6 @@ import de.tum.cit.aet.artemis.assessment.domain.Result; import de.tum.cit.aet.artemis.assessment.repository.ResultRepository; -import de.tum.cit.aet.artemis.atlas.repository.CompetencyExerciseLinkRepository; import de.tum.cit.aet.artemis.core.config.Constants; import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.core.dto.SearchResultPageDTO; @@ -87,12 +86,10 @@ public class QuizExerciseService extends QuizService { private final ExerciseService exerciseService; - private final CompetencyExerciseLinkRepository competencyExerciseLinkRepository; - public QuizExerciseService(QuizExerciseRepository quizExerciseRepository, ResultRepository resultRepository, QuizSubmissionRepository quizSubmissionRepository, InstanceMessageSendService instanceMessageSendService, QuizStatisticService quizStatisticService, QuizBatchService quizBatchService, ExerciseSpecificationService exerciseSpecificationService, FileService fileService, DragAndDropMappingRepository dragAndDropMappingRepository, - ShortAnswerMappingRepository shortAnswerMappingRepository, ExerciseService exerciseService, CompetencyExerciseLinkRepository competencyExerciseLinkRepository) { + ShortAnswerMappingRepository shortAnswerMappingRepository, ExerciseService exerciseService) { super(dragAndDropMappingRepository, shortAnswerMappingRepository); this.quizExerciseRepository = quizExerciseRepository; this.resultRepository = resultRepository; @@ -103,7 +100,6 @@ public QuizExerciseService(QuizExerciseRepository quizExerciseRepository, Result this.exerciseSpecificationService = exerciseSpecificationService; this.fileService = fileService; this.exerciseService = exerciseService; - this.competencyExerciseLinkRepository = competencyExerciseLinkRepository; } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/quiz/web/QuizParticipationResource.java b/src/main/java/de/tum/cit/aet/artemis/quiz/web/QuizParticipationResource.java index fc2b4d3b3c94..da732bcdb3ff 100644 --- a/src/main/java/de/tum/cit/aet/artemis/quiz/web/QuizParticipationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/quiz/web/QuizParticipationResource.java @@ -28,7 +28,6 @@ import de.tum.cit.aet.artemis.quiz.domain.QuizExercise; import de.tum.cit.aet.artemis.quiz.repository.QuizExerciseRepository; import de.tum.cit.aet.artemis.quiz.repository.QuizSubmissionRepository; -import de.tum.cit.aet.artemis.quiz.repository.SubmittedAnswerRepository; import de.tum.cit.aet.artemis.quiz.service.QuizBatchService; /** @@ -49,20 +48,16 @@ public class QuizParticipationResource { private final ResultRepository resultRepository; - private final SubmittedAnswerRepository submittedAnswerRepository; - private final QuizSubmissionRepository quizSubmissionRepository; private final QuizBatchService quizBatchService; public QuizParticipationResource(QuizExerciseRepository quizExerciseRepository, ParticipationService participationService, UserRepository userRepository, - ResultRepository resultRepository, SubmittedAnswerRepository submittedAnswerRepository, QuizSubmissionRepository quizSubmissionRepository, - QuizBatchService quizBatchService) { + ResultRepository resultRepository, QuizSubmissionRepository quizSubmissionRepository, QuizBatchService quizBatchService) { this.quizExerciseRepository = quizExerciseRepository; this.participationService = participationService; this.userRepository = userRepository; this.resultRepository = resultRepository; - this.submittedAnswerRepository = submittedAnswerRepository; this.quizSubmissionRepository = quizSubmissionRepository; this.quizBatchService = quizBatchService; } diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml index f2a2506dc1f5..78ae88017f7f 100644 --- a/src/main/resources/config/application.yml +++ b/src/main/resources/config/application.yml @@ -71,20 +71,20 @@ artemis: empty: default: "ubuntu:24.04" python: - default: "ls1tum/artemis-python-docker:latest" + default: "ls1tum/artemis-python-docker:v1.0.0" c: # possible overrides: gcc, fact - default: "ls1tum/artemis-c-docker:latest" - fact: "sharingcodeability/fact:latest" + default: "ls1tum/artemis-c-docker:v1.0.0" + fact: "sharingcodeability/fact:v0.0.5" haskell: default: "ghcr.io/uni-passau-artemis/artemis-haskell:v22.37.0" vhdl: - default: "tizianleonhardt/era-artemis-vhdl:latest" + default: "ghcr.io/ls1intum/artemis-vhdl-docker:v1.0.0" assembler: - default: "tizianleonhardt/era-artemis-assembler:latest" + default: "ghcr.io/ls1intum/artemis-assembler-docker:v1.0.0" swift: # possible overrides: xcode - default: "ls1tum/artemis-swift-swiftlint-docker:latest" + default: "ls1tum/artemis-swift-swiftlint-docker:swift5.9.2" ocaml: default: "ls1tum/artemis-ocaml-docker:v1" rust: @@ -98,7 +98,7 @@ artemis: c_sharp: default: "ghcr.io/ls1intum/artemis-csharp-docker:v1.0.0" typescript: - default: "ghcr.io/ls1intum/artemis-javascript-docker:v1.0.0" + default: "ghcr.io/ls1intum/artemis-javascript-docker:v1.0.0" # The following properties are used to configure the Artemis build agent. # The build agent is responsible for executing the buildJob to test student submissions. diff --git a/src/main/resources/templates/haskell/test/test/Test.hs b/src/main/resources/templates/haskell/test/test/Test.hs index 024ec6008dfd..3f1ec81468e8 100644 --- a/src/main/resources/templates/haskell/test/test/Test.hs +++ b/src/main/resources/templates/haskell/test/test/Test.hs @@ -5,7 +5,9 @@ import qualified Interface as Sub import qualified Solution as Sol import Test.Tasty -import Test.Tasty.Runners.AntXML +import Test.Tasty.Ingredients (composeReporters) +import Test.Tasty.Ingredients.Basic (consoleTestReporter) +import Test.Tasty.Runners.AntXML import Test.SmallCheck.Series as SCS import Test.Tasty.SmallCheck as SC import Test.Tasty.QuickCheck as QC @@ -69,12 +71,12 @@ main = do testRunner $ localOption timeoutOption tests where resultsPath = "test-reports/results.xml" -#ifdef PROD - -- on the server (production mode), run tests with xml output - testRunner = defaultMainWithIngredients [antXMLRunner] +#ifdef PROD + -- on the server (production mode), run tests with additional xml output + testRunner = defaultMainWithIngredients [composeReporters antXMLRunner consoleTestReporter] #else -- locally, run tests with terminal output testRunner = defaultMain -#endif +#endif -- by default, run for 1 second timeoutOption = mkTimeout (1 * 10^6) diff --git a/src/main/webapp/app/detail-overview-list/detail-overview-list.component.html b/src/main/webapp/app/detail-overview-list/detail-overview-list.component.html index 11e5ee4c828f..ba2349c564d3 100644 --- a/src/main/webapp/app/detail-overview-list/detail-overview-list.component.html +++ b/src/main/webapp/app/detail-overview-list/detail-overview-list.component.html @@ -1,7 +1,7 @@ @if (headlines?.length && headlines.length > 1) { } -@for (section of sections; track section) { +@for (section of sections(); track section) {

{{ section.headline | artemisTranslate }}

@for (detail of section.details; track $index) { diff --git a/src/main/webapp/app/detail-overview-list/detail-overview-list.component.ts b/src/main/webapp/app/detail-overview-list/detail-overview-list.component.ts index 25a5a6ca72e6..27eeca162243 100644 --- a/src/main/webapp/app/detail-overview-list/detail-overview-list.component.ts +++ b/src/main/webapp/app/detail-overview-list/detail-overview-list.component.ts @@ -1,7 +1,7 @@ -import { Component, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; +import { Component, OnDestroy, OnInit, ViewEncapsulation, inject, input } from '@angular/core'; import { isEmpty } from 'lodash-es'; import { FeatureToggle } from 'app/shared/feature-toggle/feature-toggle.service'; -import { ButtonSize, TooltipPlacement } from 'app/shared/components/button.component'; +import { ButtonSize } from 'app/shared/components/button.component'; import { IrisSubSettingsType } from 'app/entities/iris/settings/iris-sub-settings.model'; import { ModelingExerciseService } from 'app/exercises/modeling/manage/modeling-exercise.service'; import { AlertService } from 'app/core/util/alert.service'; @@ -50,11 +50,13 @@ export class DetailOverviewListComponent implements OnInit, OnDestroy { protected readonly FeatureToggle = FeatureToggle; protected readonly ButtonSize = ButtonSize; protected readonly ProgrammingExerciseParticipationType = ProgrammingExerciseParticipationType; + protected readonly CHAT = IrisSubSettingsType.CHAT; - readonly CHAT = IrisSubSettingsType.CHAT; + private readonly modelingExerciseService = inject(ModelingExerciseService); + private readonly alertService = inject(AlertService); + private readonly profileService = inject(ProfileService); - @Input() - sections: DetailOverviewSection[]; + sections = input.required(); // headline list for navigation bar headlines: { id: string; translationKey: string }[]; @@ -64,14 +66,8 @@ export class DetailOverviewListComponent implements OnInit, OnDestroy { profileSubscription: Subscription; isLocalVC = false; - constructor( - private modelingExerciseService: ModelingExerciseService, - private alertService: AlertService, - private profileService: ProfileService, - ) {} - ngOnInit() { - this.headlines = this.sections.map((section) => { + this.headlines = this.sections().map((section) => { return { id: section.headline.replaceAll('.', '-'), translationKey: section.headline, @@ -98,6 +94,4 @@ export class DetailOverviewListComponent implements OnInit, OnDestroy { ngOnDestroy() { this.profileSubscription?.unsubscribe(); } - - protected readonly TooltipPlacement = TooltipPlacement; } diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts b/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts index 1a1a95462530..baed01da2383 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts @@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { SafeHtml } from '@angular/platform-browser'; import { ProgrammingExerciseBuildConfig } from 'app/entities/programming/programming-exercise-build.config'; -import { Subject, Subscription } from 'rxjs'; +import { Subject, Subscription, of } from 'rxjs'; import { ProgrammingExercise, ProgrammingLanguage } from 'app/entities/programming/programming-exercise.model'; import { ProgrammingExerciseService } from 'app/exercises/programming/manage/services/programming-exercise.service'; import { AlertService, AlertType } from 'app/core/util/alert.service'; @@ -57,6 +57,9 @@ import { IrisSubSettingsType } from 'app/entities/iris/settings/iris-sub-setting import { Detail } from 'app/detail-overview-list/detail.model'; import { Competency } from 'app/entities/competency.model'; import { AeolusService } from 'app/exercises/programming/shared/service/aeolus.service'; +import { mergeMap, tap } from 'rxjs/operators'; +import { ProgrammingExerciseGitDiffReport } from 'app/entities/hestia/programming-exercise-git-diff-report.model'; +import { BuildLogStatisticsDTO } from 'app/entities/programming/build-log-statistics-dto'; @Component({ selector: 'jhi-programming-exercise-detail', @@ -65,15 +68,32 @@ import { AeolusService } from 'app/exercises/programming/shared/service/aeolus.s encapsulation: ViewEncapsulation.None, }) export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { - readonly dayjs = dayjs; - readonly ActionType = ActionType; - readonly ProgrammingExerciseParticipationType = ProgrammingExerciseParticipationType; - readonly FeatureToggle = FeatureToggle; - readonly ProgrammingLanguage = ProgrammingLanguage; - readonly PROGRAMMING = ExerciseType.PROGRAMMING; - readonly ButtonSize = ButtonSize; - readonly AssessmentType = AssessmentType; - readonly documentationType: DocumentationType = 'Programming'; + protected readonly dayjs = dayjs; + protected readonly ActionType = ActionType; + protected readonly ProgrammingExerciseParticipationType = ProgrammingExerciseParticipationType; + protected readonly FeatureToggle = FeatureToggle; + protected readonly ProgrammingLanguage = ProgrammingLanguage; + protected readonly PROGRAMMING = ExerciseType.PROGRAMMING; + protected readonly ButtonSize = ButtonSize; + protected readonly AssessmentType = AssessmentType; + protected readonly documentationType: DocumentationType = 'Programming'; + + protected readonly faUndo = faUndo; + protected readonly faTrash = faTrash; + protected readonly faBook = faBook; + protected readonly faWrench = faWrench; + protected readonly faCheckDouble = faCheckDouble; + protected readonly faTable = faTable; + protected readonly faExclamationTriangle = faExclamationTriangle; + protected readonly faFileSignature = faFileSignature; + protected readonly faListAlt = faListAlt; + protected readonly faChartBar = faChartBar; + protected readonly faLightbulb = faLightbulb; + protected readonly faPencilAlt = faPencilAlt; + protected readonly faUsers = faUsers; + protected readonly faEye = faEye; + protected readonly faUserCheck = faUserCheck; + protected readonly faRobot = faRobot; programmingExercise: ProgrammingExercise; programmingExerciseBuildConfig?: ProgrammingExerciseBuildConfig; @@ -106,10 +126,7 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { private activatedRouteSubscription: Subscription; private templateAndSolutionParticipationSubscription: Subscription; - private profileInfoSubscription: Subscription; private irisSettingsSubscription: Subscription; - private submissionPolicySubscription: Subscription; - private buildLogsSubscription: Subscription; private exerciseStatisticsSubscription: Subscription; private dialogErrorSource = new Subject(); @@ -117,24 +134,6 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { exerciseDetailSections: DetailOverviewSection[]; - // Icons - faUndo = faUndo; - faTrash = faTrash; - faBook = faBook; - faWrench = faWrench; - faCheckDouble = faCheckDouble; - faTable = faTable; - faExclamationTriangle = faExclamationTriangle; - faFileSignature = faFileSignature; - faListAlt = faListAlt; - faChartBar = faChartBar; - faLightbulb = faLightbulb; - faPencilAlt = faPencilAlt; - faUsers = faUsers; - faEye = faEye; - faUserCheck = faUserCheck; - faRobot = faRobot; - constructor( private activatedRoute: ActivatedRoute, private accountService: AccountService, @@ -184,13 +183,15 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { this.templateAndSolutionParticipationSubscription = this.programmingExerciseService .findWithTemplateAndSolutionParticipationAndLatestResults(programmingExercise.id!) - .subscribe((updatedProgrammingExercise) => { - this.programmingExercise = updatedProgrammingExercise.body!; - - this.setLatestCoveredLineRatio(); - this.loadingTemplateParticipationResults = false; - this.loadingSolutionParticipationResults = false; - this.profileInfoSubscription = this.profileService.getProfileInfo().subscribe(async (profileInfo) => { + .pipe( + tap((updatedProgrammingExercise) => { + this.programmingExercise = updatedProgrammingExercise.body!; + this.setLatestCoveredLineRatio(); + this.loadingTemplateParticipationResults = false; + this.loadingSolutionParticipationResults = false; + }), + mergeMap(() => this.profileService.getProfileInfo()), + tap((profileInfo) => { if (profileInfo) { if (this.programmingExercise.projectKey && this.programmingExercise.templateParticipation?.buildPlanId) { this.programmingExercise.templateParticipation.buildPlanUrl = createBuildPlanUrl( @@ -215,38 +216,41 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { if (this.irisEnabled) { this.irisSettingsSubscription = this.irisSettingsService.getCombinedCourseSettings(this.courseId).subscribe((settings) => { this.irisChatEnabled = settings?.irisChatSettings?.enabled ?? false; - this.exerciseDetailSections = this.getExerciseDetails(); }); } } + }), + mergeMap(() => this.programmingExerciseSubmissionPolicyService.getSubmissionPolicyOfProgrammingExercise(exerciseId)), + tap((submissionPolicy) => { + this.programmingExercise.submissionPolicy = submissionPolicy; + }), + mergeMap(() => this.programmingExerciseService.getDiffReport(exerciseId)), + tap((gitDiffReport) => { + this.processGitDiffReport(gitDiffReport, false); + }), + mergeMap(() => + this.programmingExercise.isAtLeastEditor ? this.programmingExerciseService.getBuildLogStatistics(exerciseId!) : of([] as BuildLogStatisticsDTO), + ), + tap((buildLogStatistics) => { + if (this.programmingExercise.isAtLeastEditor) { + this.programmingExercise.buildLogStatistics = buildLogStatistics; + } + }), + ) + .subscribe({ + next: () => { + this.setLatestCoveredLineRatio(); + this.checkAndAlertInconsistencies(); + this.plagiarismCheckSupported = this.programmingLanguageFeatureService.getProgrammingLanguageFeature( + programmingExercise.programmingLanguage, + ).plagiarismCheckSupported; + + /** we make sure to await the results of the subscriptions (switchMap) to only call {@link getExerciseDetails} once */ this.exerciseDetailSections = this.getExerciseDetails(); - }); - - this.submissionPolicySubscription = this.programmingExerciseSubmissionPolicyService - .getSubmissionPolicyOfProgrammingExercise(exerciseId!) - .subscribe((submissionPolicy) => { - this.programmingExercise.submissionPolicy = submissionPolicy; - this.exerciseDetailSections = this.getExerciseDetails(); - }); - - this.loadGitDiffReport(); - - // the build logs endpoint requires at least editor privileges - if (this.programmingExercise.isAtLeastEditor) { - this.buildLogsSubscription = this.programmingExerciseService - .getBuildLogStatistics(exerciseId!) - .subscribe((buildLogStatistics) => (this.programmingExercise.buildLogStatistics = buildLogStatistics)); - this.exerciseDetailSections = this.getExerciseDetails(); - } - - this.setLatestCoveredLineRatio(); - - this.checkAndAlertInconsistencies(); - - this.plagiarismCheckSupported = this.programmingLanguageFeatureService.getProgrammingLanguageFeature( - programmingExercise.programmingLanguage, - ).plagiarismCheckSupported; - this.exerciseDetailSections = this.getExerciseDetails(); + }, + error: (error) => { + this.alertService.error(error.message); + }, }); this.exerciseStatisticsSubscription = this.statisticsService.getExerciseStatistics(exerciseId!).subscribe((statistics: ExerciseManagementStatisticsDto) => { @@ -259,13 +263,17 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { this.dialogErrorSource.unsubscribe(); this.activatedRouteSubscription?.unsubscribe(); this.templateAndSolutionParticipationSubscription?.unsubscribe(); - this.profileInfoSubscription?.unsubscribe(); this.irisSettingsSubscription?.unsubscribe(); - this.submissionPolicySubscription?.unsubscribe(); - this.buildLogsSubscription?.unsubscribe(); this.exerciseStatisticsSubscription?.unsubscribe(); } + /** + * BE CAREFUL WHEN CALLING THIS METHOD!
+ * This method can cause child components to re-render, which can lead to re-initializations resulting + * in unnecessary requests putting load on the server. + * + * When adding a new call to this method, make sure that no duplicated and unnecessary requests are made. + */ getExerciseDetails(): DetailOverviewSection[] { const exercise = this.programmingExercise; exercise.buildConfig = this.programmingExerciseBuildConfig; @@ -780,29 +788,37 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { return link; } - loadGitDiffReport() { - this.programmingExerciseService.getDiffReport(this.programmingExercise.id!).subscribe((gitDiffReport) => { - if ( - gitDiffReport && - (this.programmingExercise.gitDiffReport?.templateRepositoryCommitHash !== gitDiffReport.templateRepositoryCommitHash || - this.programmingExercise.gitDiffReport?.solutionRepositoryCommitHash !== gitDiffReport.solutionRepositoryCommitHash) - ) { - this.programmingExercise.gitDiffReport = gitDiffReport; - gitDiffReport.programmingExercise = this.programmingExercise; - this.addedLineCount = - gitDiffReport.entries - ?.map((entry) => entry.lineCount) - .filter((lineCount) => lineCount) - .map((lineCount) => lineCount!) - .reduce((lineCount1, lineCount2) => lineCount1 + lineCount2, 0) ?? 0; - this.removedLineCount = - gitDiffReport.entries - ?.map((entry) => entry.previousLineCount) - .filter((lineCount) => lineCount) - .map((lineCount) => lineCount!) - .reduce((lineCount1, lineCount2) => lineCount1 + lineCount2, 0) ?? 0; + /** + * + * @param gitDiffReport + * @param updateDetailSections set to false when called from OnInit, as another method will take care to update the + * {@link exerciseDetailSections} to prevent unnecessary renderings and duplicated requests, + * see description of {@link getExerciseDetails} + */ + private processGitDiffReport(gitDiffReport: ProgrammingExerciseGitDiffReport | undefined, updateDetailSections: boolean = true): void { + const isGitDiffReportUpdated = + gitDiffReport && + (this.programmingExercise.gitDiffReport?.templateRepositoryCommitHash !== gitDiffReport.templateRepositoryCommitHash || + this.programmingExercise.gitDiffReport?.solutionRepositoryCommitHash !== gitDiffReport.solutionRepositoryCommitHash); + if (isGitDiffReportUpdated) { + this.programmingExercise.gitDiffReport = gitDiffReport; + gitDiffReport.programmingExercise = this.programmingExercise; + + const calculateLineCount = (entries: { lineCount?: number; previousLineCount?: number }[] = [], key: 'lineCount' | 'previousLineCount') => + entries.map((entry) => entry[key] ?? 0).reduce((sum, count) => sum + count, 0); + + this.addedLineCount = calculateLineCount(gitDiffReport.entries, 'lineCount'); + this.removedLineCount = calculateLineCount(gitDiffReport.entries, 'previousLineCount'); + + if (updateDetailSections) { this.exerciseDetailSections = this.getExerciseDetails(); } + } + } + + loadGitDiffReport() { + this.programmingExerciseService.getDiffReport(this.programmingExercise.id!).subscribe((gitDiffReport) => { + this.processGitDiffReport(gitDiffReport); }); } diff --git a/src/main/webapp/app/exercises/shared/participation/participation.component.html b/src/main/webapp/app/exercises/shared/participation/participation.component.html index c3eadf5cd3e1..27f175995eef 100644 --- a/src/main/webapp/app/exercises/shared/participation/participation.component.html +++ b/src/main/webapp/app/exercises/shared/participation/participation.component.html @@ -4,38 +4,36 @@

{{ exercise?.title }} - {{ filteredParticipationsSize }}

- @if (exercise?.type === ExerciseType.PROGRAMMING) { -
- - +
+ + + + @if (exercise.type === ExerciseType.PROGRAMMING && afterDueDate) { - @if (exercise.type === ExerciseType.PROGRAMMING && afterDueDate) { - - } -
- } + } +
@if (exercise?.type !== ExerciseType.QUIZ && exercise?.isAtLeastInstructor) {
diff --git a/src/main/webapp/app/exercises/shared/participation/participation.component.ts b/src/main/webapp/app/exercises/shared/participation/participation.component.ts index 627a33ce8815..c3e018e4b526 100644 --- a/src/main/webapp/app/exercises/shared/participation/participation.component.ts +++ b/src/main/webapp/app/exercises/shared/participation/participation.component.ts @@ -150,7 +150,7 @@ export class ParticipationComponent implements OnInit, OnDestroy { } private loadParticipations(exerciseId: number) { - this.participationService.findAllParticipationsByExercise(exerciseId, true).subscribe((participationsResponse) => { + this.participationService.findAllParticipationsByExercise(exerciseId, false).subscribe((participationsResponse) => { this.participations = participationsResponse.body!; if (this.exercise.type === ExerciseType.PROGRAMMING) { const programmingExercise = this.exercise as ProgrammingExercise; diff --git a/src/main/webapp/app/lecture/lecture-attachments.component.html b/src/main/webapp/app/lecture/lecture-attachments.component.html index c20d21a173c5..6192f270d64e 100644 --- a/src/main/webapp/app/lecture/lecture-attachments.component.html +++ b/src/main/webapp/app/lecture/lecture-attachments.component.html @@ -1,29 +1,29 @@
- @if (lecture) { + @if (lecture()) {
- @if (showHeader) { + @if (showHeader()) {
-

: {{ lecture.title }} - {{ lecture.course?.shortName }}

+

: {{ lecture().title }} - {{ lecture().course?.shortName }}


- {{ lecture.startDate | artemisDate }} + {{ lecture().startDate | artemisDate }}
- {{ lecture.endDate | artemisDate }} + {{ lecture().endDate | artemisDate }}
-
+

@@ -83,7 +83,7 @@

- @if (lecture.isAtLeastInstructor) { + @if (lecture().isAtLeastInstructor) {
- -
@@ -212,7 +212,7 @@

@if (!attachmentToBeCreated) {
-
diff --git a/src/main/webapp/app/lecture/lecture-attachments.component.ts b/src/main/webapp/app/lecture/lecture-attachments.component.ts index 6bb35a0e84af..262bbfe593a5 100644 --- a/src/main/webapp/app/lecture/lecture-attachments.component.ts +++ b/src/main/webapp/app/lecture/lecture-attachments.component.ts @@ -1,9 +1,9 @@ -import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { Component, ElementRef, OnDestroy, ViewChild, effect, inject, input, signal } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { Lecture } from 'app/entities/lecture.model'; import dayjs from 'dayjs/esm'; -import { Subject } from 'rxjs'; +import { Subject, Subscription } from 'rxjs'; import { FileService } from 'app/shared/http/file.service'; import { Attachment, AttachmentType } from 'app/entities/attachment.model'; import { AttachmentService } from 'app/lecture/attachment.service'; @@ -16,7 +16,7 @@ import { LectureService } from 'app/lecture/lecture.service'; templateUrl: './lecture-attachments.component.html', styleUrls: ['./lecture-attachments.component.scss'], }) -export class LectureAttachmentsComponent implements OnInit, OnDestroy { +export class LectureAttachmentsComponent implements OnDestroy { protected readonly faSpinner = faSpinner; protected readonly faTimes = faTimes; protected readonly faTrash = faTrash; @@ -28,11 +28,16 @@ export class LectureAttachmentsComponent implements OnInit, OnDestroy { protected readonly allowedFileExtensions = ALLOWED_FILE_EXTENSIONS_HUMAN_READABLE; protected readonly acceptedFileExtensionsFileBrowser = ACCEPTED_FILE_EXTENSIONS_FILE_BROWSER; + private readonly activatedRoute = inject(ActivatedRoute); + private readonly attachmentService = inject(AttachmentService); + private readonly lectureService = inject(LectureService); + private readonly fileService = inject(FileService); + @ViewChild('fileInput', { static: false }) fileInput: ElementRef; - @Input() lectureId: number | undefined; - @Input() showHeader = true; + lectureId = input(); + showHeader = input(true); - lecture: Lecture; + lecture = signal(new Lecture()); attachments: Attachment[] = []; attachmentToBeCreated?: Attachment; attachmentBackup?: Attachment; @@ -46,30 +51,31 @@ export class LectureAttachmentsComponent implements OnInit, OnDestroy { private dialogErrorSource = new Subject(); dialogError$ = this.dialogErrorSource.asObservable(); - constructor( - protected activatedRoute: ActivatedRoute, - private attachmentService: AttachmentService, - private lectureService: LectureService, - private fileService: FileService, - ) {} - - ngOnInit() { - this.notificationText = undefined; - this.activatedRoute.parent!.data.subscribe(({ lecture }) => { - if (this.lectureId) { - this.lectureService.findWithDetails(this.lectureId).subscribe((lectureResponse: HttpResponse) => { - this.lecture = lectureResponse.body!; - this.loadAttachments(); + private routeDataSubscription?: Subscription; + + constructor() { + effect( + () => { + this.notificationText = undefined; + this.routeDataSubscription?.unsubscribe(); // in case the subscription was already defined + this.routeDataSubscription = this.activatedRoute.parent!.data.subscribe(({ lecture }) => { + if (this.lectureId()) { + this.lectureService.findWithDetails(this.lectureId()!).subscribe((lectureResponse: HttpResponse) => { + this.lecture.set(lectureResponse.body!); + this.loadAttachments(); + }); + } else { + this.lecture.set(lecture); + this.loadAttachments(); + } }); - } else { - this.lecture = lecture; - this.loadAttachments(); - } - }); + }, + { allowSignalWrites: true }, + ); } loadAttachments(): void { - this.attachmentService.findAllByLectureId(this.lecture.id!).subscribe((attachmentsResponse: HttpResponse) => { + this.attachmentService.findAllByLectureId(this.lecture().id!).subscribe((attachmentsResponse: HttpResponse) => { this.attachments = attachmentsResponse.body!; this.attachments.forEach((attachment) => { this.viewButtonAvailable[attachment.id!] = this.isViewButtonAvailable(attachment.link!); @@ -79,6 +85,7 @@ export class LectureAttachmentsComponent implements OnInit, OnDestroy { ngOnDestroy(): void { this.dialogErrorSource.unsubscribe(); + this.routeDataSubscription?.unsubscribe(); } isViewButtonAvailable(attachmentLink: string): boolean { @@ -91,7 +98,7 @@ export class LectureAttachmentsComponent implements OnInit, OnDestroy { addAttachment(): void { const newAttachment = new Attachment(); - newAttachment.lecture = this.lecture; + newAttachment.lecture = this.lecture(); newAttachment.attachmentType = AttachmentType.FILE; newAttachment.version = 0; newAttachment.uploadDate = dayjs(); @@ -133,8 +140,8 @@ export class LectureAttachmentsComponent implements OnInit, OnDestroy { this.attachmentService.create(this.attachmentToBeCreated!, this.attachmentFile!).subscribe({ next: (attachmentRes: HttpResponse) => { this.attachments.push(attachmentRes.body!); - this.lectureService.findWithDetails(this.lecture.id!).subscribe((lectureResponse: HttpResponse) => { - this.lecture = lectureResponse.body!; + this.lectureService.findWithDetails(this.lecture().id!).subscribe((lectureResponse: HttpResponse) => { + this.lecture.set(lectureResponse.body!); }); this.attachmentFile = undefined; this.attachmentToBeCreated = undefined; diff --git a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/attachment-unit-form/attachment-unit-form.component.ts b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/attachment-unit-form/attachment-unit-form.component.ts index 6ed702b59547..1bc42524d04b 100644 --- a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/attachment-unit-form/attachment-unit-form.component.ts +++ b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/attachment-unit-form/attachment-unit-form.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, OnChanges, ViewChild, computed, inject, input, output, signal } from '@angular/core'; +import { Component, ElementRef, OnChanges, ViewChild, computed, inject, input, output, signal, viewChild } from '@angular/core'; import dayjs from 'dayjs/esm'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { faQuestionCircle, faTimes } from '@fortawesome/free-solid-svg-icons'; @@ -6,6 +6,7 @@ import { ACCEPTED_FILE_EXTENSIONS_FILE_BROWSER, ALLOWED_FILE_EXTENSIONS_HUMAN_RE import { CompetencyLectureUnitLink } from 'app/entities/competency.model'; import { MAX_FILE_SIZE } from 'app/shared/constants/input.constants'; import { toSignal } from '@angular/core/rxjs-interop'; +import { FormDateTimePickerComponent } from 'app/shared/date-time-picker/date-time-picker.component'; export interface AttachmentUnitFormData { formProperties: FormProperties; @@ -47,6 +48,8 @@ export class AttachmentUnitFormComponent implements OnChanges { hasCancelButton = input(false); onCancel = output(); + datePickerComponent = viewChild(FormDateTimePickerComponent); + // have to handle the file input as a special case at is not part of the reactive form @ViewChild('fileInput', { static: false }) fileInput: ElementRef; @@ -68,7 +71,7 @@ export class AttachmentUnitFormComponent implements OnChanges { private readonly statusChanges = toSignal(this.form.statusChanges ?? 'INVALID'); isFormValid = computed(() => { - return (this.statusChanges() === 'VALID' || this.fileName()) && !this.isFileTooBig(); + return (this.statusChanges() === 'VALID' || this.fileName()) && !this.isFileTooBig() && this.datePickerComponent()?.isValid(); }); ngOnChanges(): void { diff --git a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/online-unit-form/online-unit-form.component.ts b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/online-unit-form/online-unit-form.component.ts index 67adf4f2c8dc..241feda894e8 100644 --- a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/online-unit-form/online-unit-form.component.ts +++ b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/online-unit-form/online-unit-form.component.ts @@ -1,5 +1,5 @@ import dayjs from 'dayjs/esm'; -import { Component, OnChanges, computed, inject, input, output } from '@angular/core'; +import { Component, OnChanges, computed, inject, input, output, viewChild } from '@angular/core'; import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { faArrowLeft, faTimes } from '@fortawesome/free-solid-svg-icons'; import { map } from 'rxjs'; @@ -8,6 +8,7 @@ import { OnlineResourceDTO } from 'app/lecture/lecture-unit/lecture-unit-managem import { OnlineUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/onlineUnit.service'; import { CompetencyLectureUnitLink } from 'app/entities/competency.model'; import { toSignal } from '@angular/core/rxjs-interop'; +import { FormDateTimePickerComponent } from 'app/shared/date-time-picker/date-time-picker.component'; export interface OnlineUnitFormData { name?: string; @@ -45,6 +46,8 @@ export class OnlineUnitFormComponent implements OnChanges { hasCancelButton = input(false); onCancel = output(); + datePickerComponent = viewChild(FormDateTimePickerComponent); + urlValidator = urlValidator; private readonly formBuilder = inject(FormBuilder); @@ -59,7 +62,7 @@ export class OnlineUnitFormComponent implements OnChanges { }); private readonly statusChanges = toSignal(this.form.statusChanges ?? 'INVALID'); - isFormValid = computed(() => this.statusChanges() === 'VALID'); + isFormValid = computed(() => this.statusChanges() === 'VALID' && this.datePickerComponent()?.isValid()); get nameControl() { return this.form.get('name'); diff --git a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/text-unit-form/text-unit-form.component.ts b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/text-unit-form/text-unit-form.component.ts index b6d7b31447b2..3ed05d37b8c1 100644 --- a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/text-unit-form/text-unit-form.component.ts +++ b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/text-unit-form/text-unit-form.component.ts @@ -1,4 +1,4 @@ -import { Component, OnChanges, OnDestroy, OnInit, computed, inject, input, output } from '@angular/core'; +import { Component, OnChanges, OnDestroy, OnInit, computed, inject, input, output, viewChild } from '@angular/core'; import dayjs from 'dayjs/esm'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; @@ -8,6 +8,7 @@ import { TranslateService } from '@ngx-translate/core'; import { faTimes } from '@fortawesome/free-solid-svg-icons'; import { CompetencyLectureUnitLink } from 'app/entities/competency.model'; import { toSignal } from '@angular/core/rxjs-interop'; +import { FormDateTimePickerComponent } from 'app/shared/date-time-picker/date-time-picker.component'; export interface TextUnitFormData { name?: string; @@ -32,6 +33,8 @@ export class TextUnitFormComponent implements OnInit, OnChanges, OnDestroy { hasCancelButton = input(false); onCancel = output(); + datePickerComponent = viewChild(FormDateTimePickerComponent); + // not included in reactive form content: string | undefined; contentLoadedFromCache = false; @@ -46,7 +49,7 @@ export class TextUnitFormComponent implements OnInit, OnChanges, OnDestroy { }); private readonly statusChanges = toSignal(this.form.statusChanges ?? 'INVALID'); - isFormValid = computed(() => this.statusChanges() === 'VALID'); + isFormValid = computed(() => this.statusChanges() === 'VALID' && this.datePickerComponent()?.isValid()); private markdownChanges = new Subject(); private markdownChangesSubscription: Subscription; diff --git a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/video-unit-form/video-unit-form.component.ts b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/video-unit-form/video-unit-form.component.ts index 543526c9491b..0ffea72952dc 100644 --- a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/video-unit-form/video-unit-form.component.ts +++ b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/video-unit-form/video-unit-form.component.ts @@ -1,10 +1,11 @@ import dayjs from 'dayjs/esm'; -import { Component, computed, effect, inject, input, output, untracked } from '@angular/core'; +import { Component, computed, effect, inject, input, output, untracked, viewChild } from '@angular/core'; import { AbstractControl, FormBuilder, FormGroup, ValidationErrors, Validators } from '@angular/forms'; import urlParser from 'js-video-url-parser'; import { faArrowLeft, faTimes } from '@fortawesome/free-solid-svg-icons'; import { CompetencyLectureUnitLink } from 'app/entities/competency.model'; import { toSignal } from '@angular/core/rxjs-interop'; +import { FormDateTimePickerComponent } from 'app/shared/date-time-picker/date-time-picker.component'; export interface VideoUnitFormData { name?: string; @@ -72,6 +73,8 @@ export class VideoUnitFormComponent { hasCancelButton = input(); onCancel = output(); + datePickerComponent = viewChild(FormDateTimePickerComponent); + videoSourceUrlValidator = videoSourceUrlValidator; videoSourceTransformUrlValidator = videoSourceTransformUrlValidator; @@ -84,7 +87,7 @@ export class VideoUnitFormComponent { competencyLinks: [undefined as CompetencyLectureUnitLink[] | undefined], }); private readonly statusChanges = toSignal(this.form.statusChanges ?? 'INVALID'); - isFormValid = computed(() => this.statusChanges() === 'VALID'); + isFormValid = computed(() => this.statusChanges() === 'VALID' && this.datePickerComponent()?.isValid()); constructor() { effect(() => { diff --git a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-period.component.ts b/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-period.component.ts index 8953b9e67deb..7d9b48571621 100644 --- a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-period.component.ts +++ b/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-period.component.ts @@ -9,6 +9,4 @@ export class LectureUpdateWizardPeriodComponent { @Input() currentStep: number; @Input() lecture: Lecture; @Input() validateDatesFunction: () => void; - - constructor() {} } diff --git a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-title.component.ts b/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-title.component.ts index 2c456af3f0e7..e354df72e629 100644 --- a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-title.component.ts +++ b/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-title.component.ts @@ -11,6 +11,4 @@ export class LectureUpdateWizardTitleComponent { @Input() lecture: Lecture; domainActionsDescription = [new FormulaAction()]; - - constructor() {} } diff --git a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-units.component.html b/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-units.component.html index 9d838c196f05..3736d642d80e 100644 --- a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-units.component.html +++ b/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-units.component.html @@ -1,5 +1,5 @@
-

+

- } - @if (isAnyUnitFormOpen()) { + } @else {
@if (!isEditingLectureUnit) {

- } - @if (isEditingLectureUnit) { + } @else {

} - @if (isTextUnitFormOpen) { + @if (isTextUnitFormOpen()) { } - @if (isVideoUnitFormOpen) { + @if (isVideoUnitFormOpen()) { } - @if (isOnlineUnitFormOpen) { + @if (isOnlineUnitFormOpen()) { } - @if (isAttachmentUnitFormOpen) { + @if (isAttachmentUnitFormOpen()) { } - @if (isExerciseUnitFormOpen) { + @if (isExerciseUnitFormOpen()) { (false); + isExerciseUnitFormOpen = signal(false); + isVideoUnitFormOpen = signal(false); + isOnlineUnitFormOpen = signal(false); + isAttachmentUnitFormOpen = signal(false); currentlyProcessedTextUnit: TextUnit; currentlyProcessedVideoUnit: VideoUnit; @@ -71,33 +71,33 @@ export class LectureUpdateWizardUnitsComponent implements OnInit { switch (type) { case LectureUnitType.TEXT: - this.isTextUnitFormOpen = true; + this.isTextUnitFormOpen.set(true); break; case LectureUnitType.EXERCISE: - this.isExerciseUnitFormOpen = true; + this.isExerciseUnitFormOpen.set(true); break; case LectureUnitType.VIDEO: - this.isVideoUnitFormOpen = true; + this.isVideoUnitFormOpen.set(true); break; case LectureUnitType.ONLINE: - this.isOnlineUnitFormOpen = true; + this.isOnlineUnitFormOpen.set(true); break; case LectureUnitType.ATTACHMENT: - this.isAttachmentUnitFormOpen = true; + this.isAttachmentUnitFormOpen.set(true); break; } } - isAnyUnitFormOpen(): boolean { - return this.isTextUnitFormOpen || this.isVideoUnitFormOpen || this.isOnlineUnitFormOpen || this.isAttachmentUnitFormOpen || this.isExerciseUnitFormOpen; - } + isAnyUnitFormOpen = computed(() => { + return this.isTextUnitFormOpen() || this.isVideoUnitFormOpen() || this.isOnlineUnitFormOpen() || this.isAttachmentUnitFormOpen() || this.isExerciseUnitFormOpen(); + }); onCloseLectureUnitForms() { - this.isTextUnitFormOpen = false; - this.isVideoUnitFormOpen = false; - this.isOnlineUnitFormOpen = false; - this.isAttachmentUnitFormOpen = false; - this.isExerciseUnitFormOpen = false; + this.isTextUnitFormOpen.set(false); + this.isVideoUnitFormOpen.set(false); + this.isOnlineUnitFormOpen.set(false); + this.isAttachmentUnitFormOpen.set(false); + this.isExerciseUnitFormOpen.set(false); } createEditTextUnit(formData: TextUnitFormData) { @@ -258,11 +258,11 @@ export class LectureUpdateWizardUnitsComponent implements OnInit { this.currentlyProcessedOnlineUnit = lectureUnit as OnlineUnit; this.currentlyProcessedAttachmentUnit = lectureUnit as AttachmentUnit; - this.isTextUnitFormOpen = lectureUnit.type === LectureUnitType.TEXT; - this.isVideoUnitFormOpen = lectureUnit.type === LectureUnitType.VIDEO; - this.isExerciseUnitFormOpen = lectureUnit.type === LectureUnitType.EXERCISE; - this.isOnlineUnitFormOpen = lectureUnit.type === LectureUnitType.ONLINE; - this.isAttachmentUnitFormOpen = lectureUnit.type === LectureUnitType.ATTACHMENT; + this.isTextUnitFormOpen.set(lectureUnit.type === LectureUnitType.TEXT); + this.isVideoUnitFormOpen.set(lectureUnit.type === LectureUnitType.VIDEO); + this.isExerciseUnitFormOpen.set(lectureUnit.type === LectureUnitType.EXERCISE); + this.isOnlineUnitFormOpen.set(lectureUnit.type === LectureUnitType.ONLINE); + this.isAttachmentUnitFormOpen.set(lectureUnit.type === LectureUnitType.ATTACHMENT); switch (lectureUnit.type) { case LectureUnitType.TEXT: diff --git a/src/main/webapp/app/shared/metis/post.service.ts b/src/main/webapp/app/shared/metis/post.service.ts index a44201956253..ed859078e07c 100644 --- a/src/main/webapp/app/shared/metis/post.service.ts +++ b/src/main/webapp/app/shared/metis/post.service.ts @@ -82,16 +82,6 @@ export class PostService extends PostingService { .pipe(map(this.convertPostResponseArrayDatesFromServer)); } - /** - * gets all tags for course - * @param {number} courseId - * @return {Observable} - */ - // TODO: unused, delete - getAllPostTagsByCourseId(courseId: number): Observable> { - return this.http.get(`${this.resourceUrl}${courseId}/messages/tags`, { observe: 'response' }); - } - /** * updates a post * @param {number} courseId @@ -128,20 +118,6 @@ export class PostService extends PostingService { return this.http.delete(`${this.resourceUrl}${courseId}${PostService.getResourceEndpoint(undefined, post)}/${post.id}`, { observe: 'response' }); } - /** - * determines similar posts in a course - * @param {Post} tempPost - * @param {number} courseId - * @return {Observable>} - */ - // TODO: unused, remove - computeSimilarityScoresWithCoursePosts(tempPost: Post, courseId: number): Observable { - const copy = this.convertPostingDateFromClient(tempPost); - return this.http - .post(`${this.resourceUrl}${courseId}/posts/similarity-check`, copy, { observe: 'response' }) - .pipe(map(this.convertPostResponseArrayDatesFromServer)); - } - /** * takes an array of posts and converts the date from the server * @param {HttpResponse} res diff --git a/src/main/webapp/content/images/feature-overview/instructors/create_conduct_assess.png b/src/main/webapp/content/images/feature-overview/instructors/create_conduct_assess.png index 60f6b90af648..d1521384a17e 100644 Binary files a/src/main/webapp/content/images/feature-overview/instructors/create_conduct_assess.png and b/src/main/webapp/content/images/feature-overview/instructors/create_conduct_assess.png differ diff --git a/src/main/webapp/content/images/feature-overview/instructors/fully_configurable.png b/src/main/webapp/content/images/feature-overview/instructors/fully_configurable.png index 7637b4de916a..39c7bf88fea1 100644 Binary files a/src/main/webapp/content/images/feature-overview/instructors/fully_configurable.png and b/src/main/webapp/content/images/feature-overview/instructors/fully_configurable.png differ diff --git a/src/main/webapp/content/images/feature-overview/instructors/plagiarism.png b/src/main/webapp/content/images/feature-overview/instructors/plagiarism.png index 760edf184f2c..ab403beac09e 100644 Binary files a/src/main/webapp/content/images/feature-overview/instructors/plagiarism.png and b/src/main/webapp/content/images/feature-overview/instructors/plagiarism.png differ diff --git a/src/main/webapp/content/images/feature-overview/students/online_exams.png b/src/main/webapp/content/images/feature-overview/students/online_exams.png index 910eac9ebe96..f533c22b3a0a 100644 Binary files a/src/main/webapp/content/images/feature-overview/students/online_exams.png and b/src/main/webapp/content/images/feature-overview/students/online_exams.png differ diff --git a/src/main/webapp/content/images/feature-overview/students/quiz_exercises.png b/src/main/webapp/content/images/feature-overview/students/quiz_exercises.png index 161fe6bec160..084ba7ec5ff8 100644 Binary files a/src/main/webapp/content/images/feature-overview/students/quiz_exercises.png and b/src/main/webapp/content/images/feature-overview/students/quiz_exercises.png differ diff --git a/src/main/webapp/content/images/feature-overview/students/user_interface.png b/src/main/webapp/content/images/feature-overview/students/user_interface.png index fe54735c3959..53189a84e07d 100644 Binary files a/src/main/webapp/content/images/feature-overview/students/user_interface.png and b/src/main/webapp/content/images/feature-overview/students/user_interface.png differ diff --git a/src/main/webapp/content/images/feature-overview/students/workingOffline.png b/src/main/webapp/content/images/feature-overview/students/workingOffline.png index 09828e36138c..cd9e91e74036 100644 Binary files a/src/main/webapp/content/images/feature-overview/students/workingOffline.png and b/src/main/webapp/content/images/feature-overview/students/workingOffline.png differ diff --git a/src/test/java/de/tum/cit/aet/artemis/assessment/AssessmentTeamComplaintIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/assessment/AssessmentTeamComplaintIntegrationTest.java index 9c27ccab0d46..b58435f9248d 100644 --- a/src/test/java/de/tum/cit/aet/artemis/assessment/AssessmentTeamComplaintIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/assessment/AssessmentTeamComplaintIntegrationTest.java @@ -73,10 +73,6 @@ class AssessmentTeamComplaintIntegrationTest extends AbstractSpringIntegrationIn private ComplaintRequestDTO complaintRequest; - private Complaint moreFeedbackRequest; - - private Course course; - private Team team; private final String resourceUrl = "/api/complaints"; @@ -85,7 +81,7 @@ class AssessmentTeamComplaintIntegrationTest extends AbstractSpringIntegrationIn void initTestCase() throws Exception { userUtilService.addUsers(TEST_PREFIX, 1, 2, 0, 1); // Initialize with 3 max team complaints and 7 days max complaint due date - course = modelingExerciseUtilService.addCourseWithOneModelingExercise(); + Course course = modelingExerciseUtilService.addCourseWithOneModelingExercise(); modelingExercise = (ModelingExercise) course.getExercises().iterator().next(); modelingExercise.setMode(ExerciseMode.TEAM); modelingExercise = exerciseRepository.save(modelingExercise); @@ -93,7 +89,6 @@ void initTestCase() throws Exception { saveModelingSubmissionAndAssessment(); complaint = new Complaint().result(modelingAssessment).complaintText("This is not fair").complaintType(ComplaintType.COMPLAINT); complaintRequest = new ComplaintRequestDTO(complaint.getResult().getId(), complaint.getComplaintText(), complaint.getComplaintType(), Optional.empty()); - moreFeedbackRequest = new Complaint().result(modelingAssessment).complaintText("Please explain").complaintType(ComplaintType.MORE_FEEDBACK); } @Test diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/learningpath/util/LearningPathUtilService.java b/src/test/java/de/tum/cit/aet/artemis/atlas/learningpath/util/LearningPathUtilService.java index fc922e932d68..93a099824cf6 100644 --- a/src/test/java/de/tum/cit/aet/artemis/atlas/learningpath/util/LearningPathUtilService.java +++ b/src/test/java/de/tum/cit/aet/artemis/atlas/learningpath/util/LearningPathUtilService.java @@ -1,14 +1,8 @@ package de.tum.cit.aet.artemis.atlas.learningpath.util; -import java.util.Set; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import de.tum.cit.aet.artemis.atlas.domain.competency.CourseCompetency; -import de.tum.cit.aet.artemis.atlas.domain.competency.LearningPath; -import de.tum.cit.aet.artemis.atlas.repository.CompetencyRepository; -import de.tum.cit.aet.artemis.atlas.repository.CourseCompetencyRepository; import de.tum.cit.aet.artemis.atlas.service.learningpath.LearningPathService; import de.tum.cit.aet.artemis.atlas.test_repository.LearningPathTestRepository; import de.tum.cit.aet.artemis.core.domain.Course; @@ -30,12 +24,6 @@ public class LearningPathUtilService { @Autowired private LearningPathTestRepository learningPathRepository; - @Autowired - private CompetencyRepository competencyRepository; - - @Autowired - private CourseCompetencyRepository courseCompetencyRepository; - /** * Enable and generate learning paths for course. * @@ -49,43 +37,6 @@ public Course enableAndGenerateLearningPathsForCourse(Course course) { return courseRepository.save(eagerlyLoadedCourse); } - /** - * Creates learning path for course. - * - * @param course the course for which the learning path should be generated - * @return the persisted learning path - */ - public LearningPath createLearningPathInCourse(Course course) { - final var competencies = courseCompetencyRepository.findAllForCourse(course.getId()); - LearningPath learningPath = createLearningPath(competencies); - learningPath.setCourse(course); - return learningPathRepository.save(learningPath); - } - - /** - * Creates learning path for given user in course. - * - * @param course the course for which the learning path should be generated - * @return the persisted learning path - */ - public LearningPath createLearningPathInCourseForUser(Course course, User user) { - final var learningPath = createLearningPathInCourse(course); - learningPath.setUser(user); - return learningPathRepository.save(learningPath); - } - - /** - * Creates learning path. - * - * @param competencies the competencies that will be linked to the learning path - * @return the persisted learning path - */ - public LearningPath createLearningPath(Set competencies) { - LearningPath learningPath = new LearningPath(); - learningPath.setCompetencies(competencies); - return learningPathRepository.save(learningPath); - } - /** * Deletes all learning paths of given user. * @@ -95,12 +46,4 @@ public void deleteLearningPaths(User user) { learningPathRepository.deleteAll(user.getLearningPaths()); } - /** - * Deletes all learning paths of all given user. - * - * @param users the users for which all learning paths should be deleted - */ - public void deleteLearningPaths(Iterable users) { - users.forEach(this::deleteLearningPaths); - } } diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/service/LearningPathServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/atlas/service/LearningPathServiceTest.java index d18a5ed65662..31227e22ff7e 100644 --- a/src/test/java/de/tum/cit/aet/artemis/atlas/service/LearningPathServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/atlas/service/LearningPathServiceTest.java @@ -11,24 +11,17 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import de.tum.cit.aet.artemis.assessment.util.StudentScoreUtilService; -import de.tum.cit.aet.artemis.atlas.competency.util.CompetencyProgressUtilService; import de.tum.cit.aet.artemis.atlas.competency.util.CompetencyUtilService; import de.tum.cit.aet.artemis.atlas.domain.competency.RelationType; import de.tum.cit.aet.artemis.atlas.dto.LearningPathHealthDTO; import de.tum.cit.aet.artemis.atlas.learningpath.util.LearningPathUtilService; -import de.tum.cit.aet.artemis.atlas.repository.CompetencyRepository; import de.tum.cit.aet.artemis.atlas.service.learningpath.LearningPathRecommendationService; import de.tum.cit.aet.artemis.atlas.service.learningpath.LearningPathService; import de.tum.cit.aet.artemis.core.domain.Course; -import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.core.security.SecurityUtils; import de.tum.cit.aet.artemis.core.user.util.UserUtilService; import de.tum.cit.aet.artemis.core.util.CourseFactory; import de.tum.cit.aet.artemis.core.util.CourseUtilService; -import de.tum.cit.aet.artemis.lecture.repository.LectureUnitRepository; -import de.tum.cit.aet.artemis.lecture.util.LectureUtilService; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; class LearningPathServiceTest extends AbstractSpringIntegrationIndependentTest { @@ -50,28 +43,8 @@ class LearningPathServiceTest extends AbstractSpringIntegrationIndependentTest { @Autowired private CompetencyUtilService competencyUtilService; - @Autowired - private LectureUtilService lectureUtilService; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private CompetencyRepository competencyRepository; - - @Autowired - private CompetencyProgressUtilService competencyProgressUtilService; - - @Autowired - private StudentScoreUtilService studentScoreUtilService; - - @Autowired - private LectureUnitRepository lectureUnitRepository; - private Course course; - private User user; - @BeforeEach void setAuthorizationForRepositoryRequests() { SecurityUtils.setAuthorizationObject(); @@ -129,8 +102,7 @@ class GenerateNgxPathRepresentation { @BeforeEach void setup() { - final var users = userUtilService.addUsers(TEST_PREFIX, 1, 0, 0, 0); - user = users.getFirst(); + userUtilService.addUsers(TEST_PREFIX, 1, 0, 0, 0); course = CourseFactory.generateCourse(null, ZonedDateTime.now().minusDays(8), ZonedDateTime.now().minusDays(8), new HashSet<>(), TEST_PREFIX + "tumuser", TEST_PREFIX + "tutor", TEST_PREFIX + "editor", TEST_PREFIX + "instructor"); course = courseRepository.save(course); diff --git a/src/test/java/de/tum/cit/aet/artemis/communication/MessageIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/communication/MessageIntegrationTest.java index a23b10fd360d..d9b8f3be40e1 100644 --- a/src/test/java/de/tum/cit/aet/artemis/communication/MessageIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/communication/MessageIntegrationTest.java @@ -9,8 +9,6 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.time.ZonedDateTime; import java.util.List; @@ -32,24 +30,17 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.test.context.TestSecurityContextHolder; import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.web.servlet.MvcResult; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import com.fasterxml.jackson.databind.ObjectMapper; - import de.tum.cit.aet.artemis.communication.domain.AnswerPost; import de.tum.cit.aet.artemis.communication.domain.ConversationParticipant; import de.tum.cit.aet.artemis.communication.domain.DisplayPriority; import de.tum.cit.aet.artemis.communication.domain.Post; import de.tum.cit.aet.artemis.communication.domain.PostSortCriterion; import de.tum.cit.aet.artemis.communication.domain.conversation.Channel; +import de.tum.cit.aet.artemis.communication.domain.conversation.Conversation; import de.tum.cit.aet.artemis.communication.domain.conversation.OneToOneChat; import de.tum.cit.aet.artemis.communication.domain.notification.ConversationNotification; import de.tum.cit.aet.artemis.communication.domain.notification.Notification; @@ -84,9 +75,6 @@ class MessageIntegrationTest extends AbstractSpringIntegrationIndependentTest { @Autowired private ConversationParticipantTestRepository conversationParticipantRepository; - @Autowired - private ObjectMapper objectMapper; - @Autowired private ConversationUtilService conversationUtilService; @@ -316,9 +304,8 @@ void testBroadCastWithNotification() throws Exception { @ValueSource(ints = { HIGHER_PAGE_SIZE, LOWER_PAGE_SIZE, EQUAL_PAGE_SIZE }) @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void testFindMessagesWithPageSizes(int pageSize) { - - var student1 = userTestRepository.findOneWithGroupsAndAuthoritiesByLogin(TEST_PREFIX + "student1").orElseThrow(); - var student2 = userTestRepository.findOneWithGroupsAndAuthoritiesByLogin(TEST_PREFIX + "student2").orElseThrow(); + var student1 = userTestRepository.findOneByLogin(TEST_PREFIX + "student1").orElseThrow(); + var student2 = userTestRepository.findOneByLogin(TEST_PREFIX + "student2").orElseThrow(); List posts = conversationUtilService.createPostsWithAnswersAndReactionsAndConversation(course, student1, student2, NUMBER_OF_POSTS, TEST_PREFIX); long conversationId = posts.getFirst().getConversation().getId(); for (Post post : posts) { @@ -422,7 +409,7 @@ void testGetConversationPost() throws Exception { List returnedPosts = request.getList("/api/courses/" + courseId + "/messages", HttpStatus.OK, Post.class, params); // get amount of posts with that certain - assertThat(returnedPosts).hasSize(existingConversationMessages.stream().filter(post -> post.getConversation().getId() == conversationId).toList().size()); + assertThat(returnedPosts).hasSize(existingConversationMessages.stream().filter(post -> Objects.equals(post.getConversation().getId(), conversationId)).toList().size()); } @Test @@ -602,19 +589,18 @@ void testPinPost_asTutor() throws Exception { @WithMockUser(username = TEST_PREFIX + "tutor1") void testEditConversationPostWithUserMention(String userMention, boolean isUserMentionValid) throws Exception { // conversation post of tutor1 must be only editable by them - Post conversationPostToUpdate = existingConversationMessages.getFirst(); - conversationPostToUpdate.setContent("User changes one of their conversation posts" + userMention); + Post postToUpdate = existingConversationMessages.getFirst(); + postToUpdate.setContent("User changes one of their conversation posts" + userMention); if (!isUserMentionValid) { - request.putWithResponseBody("/api/courses/" + courseId + "/messages/" + conversationPostToUpdate.getId(), conversationPostToUpdate, Post.class, HttpStatus.BAD_REQUEST); + request.putWithResponseBody("/api/courses/" + courseId + "/messages/" + postToUpdate.getId(), postToUpdate, Post.class, HttpStatus.BAD_REQUEST); verify(websocketMessagingService, never()).sendMessageToUser(anyString(), anyString(), any(PostDTO.class)); return; } - Post updatedPost = request.putWithResponseBody("/api/courses/" + courseId + "/messages/" + conversationPostToUpdate.getId(), conversationPostToUpdate, Post.class, - HttpStatus.OK); + Post updatedPost = request.putWithResponseBody("/api/courses/" + courseId + "/messages/" + postToUpdate.getId(), postToUpdate, Post.class, HttpStatus.OK); - assertThat(conversationPostToUpdate).isEqualTo(updatedPost); + assertThat(postToUpdate).isEqualTo(updatedPost); // both conversation participants should be notified about the update verify(websocketMessagingService, timeout(2000).times(2)).sendMessage(anyString(), @@ -665,84 +651,93 @@ void testDeleteConversationPost_forbidden() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void testIncreaseUnreadMessageCountAfterMessageSend() throws Exception { + var student1 = userTestRepository.findOneByLogin(TEST_PREFIX + "student1").orElseThrow(); + var student2 = userTestRepository.findOneByLogin(TEST_PREFIX + "student2").orElseThrow(); + Post postToSave = createPostWithOneToOneChat(TEST_PREFIX); Post createdPost = createPostAndAwaitAsyncCode(postToSave); await().untilAsserted(() -> { SecurityUtils.setAuthorizationObject(); - long unreadMessages = oneToOneChatRepository.findByIdWithConversationParticipantsAndUserGroups(createdPost.getConversation().getId()).orElseThrow() - .getConversationParticipants().stream() - .filter(conversationParticipant -> !Objects.equals(conversationParticipant.getUser().getId(), postToSave.getAuthor().getId())).findAny().orElseThrow() - .getUnreadMessagesCount(); - assertThat(unreadMessages).isEqualTo(1L); + // student1 (author) should have 0 unread messages, student2 (conversation participant) should have 1 unread message + assertThat(getUnreadMessagesCount(createdPost.getConversation(), student1)).isZero(); + assertThat(getUnreadMessagesCount(createdPost.getConversation(), student2)).isEqualTo(1); }); } @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void testDecreaseUnreadMessageCountAfterMessageRead() throws Exception { - Post postToSave1 = createPostWithOneToOneChat(TEST_PREFIX); - - ResultActions resultActions = request - .performMvcRequest(MockMvcRequestBuilders.post("/api/courses/" + courseId + "/messages").contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(postToSave1)).with(user(TEST_PREFIX + "student1").roles("USER")).accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()); + var student1 = userTestRepository.findOneByLogin(TEST_PREFIX + "student1").orElseThrow(); + var student2 = userTestRepository.findOneByLogin(TEST_PREFIX + "student2").orElseThrow(); - MvcResult result = resultActions.andReturn(); - String contentAsString = result.getResponse().getContentAsString(); - Post createdPost1 = objectMapper.readValue(contentAsString, Post.class); + Post postToSave1 = createPostWithOneToOneChat(TEST_PREFIX); + Post createdPost1 = request.postWithResponseBody("/api/courses/" + courseId + "/messages", postToSave1, Post.class, HttpStatus.CREATED); - request.performMvcRequest(MockMvcRequestBuilders.get("/api/courses/" + courseId + "/messages").param("conversationId", createdPost1.getConversation().getId().toString()) - .param("pagingEnabled", "true").param("size", String.valueOf(MAX_POSTS_PER_PAGE)).with(user(TEST_PREFIX + "student2").roles("USER")) - .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + userUtilService.changeUser(TEST_PREFIX + "student2"); + // we read the messages by "getting" them from the server as student + var params = new LinkedMultiValueMap(); + params.add("conversationId", createdPost1.getConversation().getId().toString()); + params.add("pagingEnabled", "true"); + params.add("size", String.valueOf(MAX_POSTS_PER_PAGE)); + Set returnedPosts = request.getSet("/api/courses/" + courseId + "/messages", HttpStatus.OK, Post.class, params); + assertThat(returnedPosts).hasSize(1); await().untilAsserted(() -> { SecurityUtils.setAuthorizationObject(); - assertThat(oneToOneChatRepository.findByIdWithConversationParticipantsAndUserGroups(createdPost1.getConversation().getId()).orElseThrow().getConversationParticipants() - .stream().filter(conversationParticipant -> !Objects.equals(conversationParticipant.getUser().getId(), postToSave1.getAuthor().getId())).findAny().orElseThrow() - .getUnreadMessagesCount()).isZero(); + assertThat(getUnreadMessagesCount(createdPost1.getConversation(), student2)).isZero(); + assertThat(getUnreadMessagesCount(createdPost1.getConversation(), student1)).isZero(); }); } @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void testDecreaseUnreadMessageCountWhenDeletingMessage() throws Exception { - Post postToSave1 = createPostWithOneToOneChat(TEST_PREFIX); - Post postToSave2 = createPostWithOneToOneChat(TEST_PREFIX); - - ResultActions resultActions = request - .performMvcRequest(MockMvcRequestBuilders.post("/api/courses/" + courseId + "/messages").contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(postToSave1)).with(user(TEST_PREFIX + "student1").roles("USER")).accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()); + final var student1 = userTestRepository.findOneByLogin(TEST_PREFIX + "student1").orElseThrow(); + final var student2 = userTestRepository.findOneByLogin(TEST_PREFIX + "student2").orElseThrow(); - MvcResult result = resultActions.andReturn(); - String contentAsString = result.getResponse().getContentAsString(); - Post createdPost1 = objectMapper.readValue(contentAsString, Post.class); + Post postToSave1 = createPostWithOneToOneChat(TEST_PREFIX); // OneToOneChat 1 + Post postToSave2 = createPostWithOneToOneChat(TEST_PREFIX); // OneToOneChat 1 - ResultActions resultActions2 = request - .performMvcRequest(MockMvcRequestBuilders.post("/api/courses/" + courseId + "/messages").contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(postToSave2)).with(user(TEST_PREFIX + "student1").roles("USER")).accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()); + Post createdPost1 = request.postWithResponseBody("/api/courses/" + courseId + "/messages", postToSave1, Post.class, HttpStatus.CREATED); + final var oneToOneChat1 = createdPost1.getConversation(); + // student 1 adds a message, so the unread count for student 2 should be 1 + await().untilAsserted(() -> { + SecurityUtils.setAuthorizationObject(); + assertThat(getUnreadMessagesCount(oneToOneChat1, student1)).isZero(); + assertThat(getUnreadMessagesCount(oneToOneChat1, student2)).isEqualTo(1); + }); - MvcResult result2 = resultActions2.andReturn(); - String contentAsString2 = result2.getResponse().getContentAsString(); - Post createdPost2 = objectMapper.readValue(contentAsString2, Post.class); + Post createdPost2 = request.postWithResponseBody("/api/courses/" + courseId + "/messages", postToSave2, Post.class, HttpStatus.CREATED); + final var oneToOneChat2 = createdPost2.getConversation(); + // student 1 adds a message, so the unread count for student 2 should be 1 + await().untilAsserted(() -> { + SecurityUtils.setAuthorizationObject(); + assertThat(getUnreadMessagesCount(oneToOneChat1, student1)).isZero(); + assertThat(getUnreadMessagesCount(oneToOneChat1, student2)).isEqualTo(1); + }); - request.performMvcRequest(MockMvcRequestBuilders.delete("/api/courses/" + courseId + "/messages/" + createdPost2.getId()).with(user(TEST_PREFIX + "student1").roles("USER")) - .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + request.delete("/api/courses/" + courseId + "/messages/" + createdPost2.getId(), HttpStatus.OK); + // After deleting the message in the second chat, the unread count in the first chat should stay the same, the unread count in the second chat will become 0 + await().untilAsserted(() -> { + SecurityUtils.setAuthorizationObject(); + assertThat(getUnreadMessagesCount(oneToOneChat1, student2)).isEqualTo(1); + assertThat(getUnreadMessagesCount(oneToOneChat2, student2)).isZero(); - SecurityContextHolder.setContext(TestSecurityContextHolder.getContext()); - long unreadMessages = oneToOneChatRepository.findByIdWithConversationParticipantsAndUserGroups(createdPost1.getConversation().getId()).orElseThrow() - .getConversationParticipants().stream() - .filter(conversationParticipant -> !Objects.equals(conversationParticipant.getUser().getId(), postToSave1.getAuthor().getId())).findAny().orElseThrow() - .getUnreadMessagesCount(); + // no changes for student1 + assertThat(getUnreadMessagesCount(oneToOneChat1, student1)).isZero(); + assertThat(getUnreadMessagesCount(oneToOneChat1, student1)).isZero(); + }); + } - assertThat(unreadMessages).isEqualTo(1); + private long getUnreadMessagesCount(Conversation conversation, User user) { + return oneToOneChatRepository.findByIdWithConversationParticipantsAndUserGroups(conversation.getId()).orElseThrow().getConversationParticipants().stream() + .filter(conversationParticipant -> Objects.equals(conversationParticipant.getUser().getId(), user.getId())).findFirst().orElseThrow().getUnreadMessagesCount(); } private Post createPostWithOneToOneChat(String userPrefix) { - var student1 = userTestRepository.findOneWithGroupsAndAuthoritiesByLogin(userPrefix + "student1").orElseThrow(); - var student2 = userTestRepository.findOneWithGroupsAndAuthoritiesByLogin(userPrefix + "student2").orElseThrow(); + var student1 = userTestRepository.findOneByLogin(userPrefix + "student1").orElseThrow(); + var student2 = userTestRepository.findOneByLogin(userPrefix + "student2").orElseThrow(); var chat = new OneToOneChat(); chat.setCourse(course); chat.setCreator(student1); @@ -810,29 +805,4 @@ private Post createPostAndAwaitAsyncCode(Post postToSave) throws Exception { private static List courseInformationSharingConfigurationProvider() { return List.of(CourseInformationSharingConfiguration.COMMUNICATION_ONLY, CourseInformationSharingConfiguration.COMMUNICATION_AND_MESSAGING); } - - @Test - @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void testSimilarityCheck() throws Exception { - Post postToCheck = new Post(); - postToCheck.setTitle("Title Post"); - - List similarPosts = request.postListWithResponseBody("/api/courses/" + courseId + "/messages/similarity-check", postToCheck, Post.class, HttpStatus.OK); - assertThat(similarPosts).hasSize(5); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void testGetPostTagsForCourse() throws Exception { - List returnedTags = request.getList("/api/courses/" + courseId + "/messages/tags", HttpStatus.OK, String.class); - // 4 different tags were used for the posts - assertThat(returnedTags).hasSameSizeAs(conversationMessageRepository.findPostTagsForCourse(courseId)); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void testGetPostTagsForCourseWithNonExistentCourseId_notFound() throws Exception { - List returnedTags = request.getList("/api/courses/" + 9999L + "/messages/tags", HttpStatus.NOT_FOUND, String.class); - assertThat(returnedTags).isNull(); - } } diff --git a/src/test/java/de/tum/cit/aet/artemis/communication/similarity/TitleJaccardSimilarityCompareStrategyTest.java b/src/test/java/de/tum/cit/aet/artemis/communication/similarity/TitleJaccardSimilarityCompareStrategyTest.java deleted file mode 100644 index 516f3ee01137..000000000000 --- a/src/test/java/de/tum/cit/aet/artemis/communication/similarity/TitleJaccardSimilarityCompareStrategyTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package de.tum.cit.aet.artemis.communication.similarity; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import de.tum.cit.aet.artemis.communication.domain.Post; -import de.tum.cit.aet.artemis.communication.service.similarity.TitleJaccardSimilarityCompareStrategy; - -class TitleJaccardSimilarityCompareStrategyTest { - - private TitleJaccardSimilarityCompareStrategy compareStrategy; - - @BeforeEach - void setUp() { - compareStrategy = new TitleJaccardSimilarityCompareStrategy(); - } - - @Test - void testSimilarTitle_equal() { - Post post1 = new Post(); - post1.setTitle("Some title"); - Post post2 = new Post(); - post2.setTitle("Title some"); - Double expectedResult = 1.0; - - Double actualResult = compareStrategy.performSimilarityCheck(post1, post2); - - assertThat(actualResult).isEqualTo(expectedResult); - } - - @Test - void testSimilarTitle_different() { - Post post1 = new Post(); - post1.setTitle("Totally different"); - Post post2 = new Post(); - post2.setTitle("Something else"); - - Double actualResult = compareStrategy.performSimilarityCheck(post1, post2); - - assertThat(actualResult).isLessThan(0.5); - } - - @Test - void testSimilarTitle_similar() { - Post post1 = new Post(); - post1.setTitle("Totally different"); - Post post2 = new Post(); - post2.setTitle("Somewhat different"); - - Double actualResult = compareStrategy.performSimilarityCheck(post1, post2); - - assertThat(actualResult).isGreaterThan(0.5).isLessThan(1); - } - - @Test - void testSimilarTitle_missing() { - Post post1 = new Post(); - Post post2 = new Post(); - Double expectedResult = 0.0; - - Double actualResult = compareStrategy.performSimilarityCheck(post1, post2); - - assertThat(actualResult).isEqualTo(expectedResult); - } -} diff --git a/src/test/java/de/tum/cit/aet/artemis/core/authentication/UserAccountLocalVcsIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/core/authentication/UserAccountLocalVcsIntegrationTest.java index a3313ce7c592..76e303845b06 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/authentication/UserAccountLocalVcsIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/authentication/UserAccountLocalVcsIntegrationTest.java @@ -38,6 +38,12 @@ void getAndCreateParticipationVcsAccessTokenByUser() throws Exception { userTestService.getAndCreateParticipationVcsAccessToken(); } + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void getParticipationVcsAccessTokenByUserForTeamExercise() throws Exception { + userTestService.getAndCreateParticipationVcsAccessTokenForTeamExercise(); + } + @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void createAndDeleteUserVcsAccessTokenByUser() throws Exception { diff --git a/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java b/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java index 77ea61f417ee..f36bf992c4eb 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java @@ -46,7 +46,11 @@ import de.tum.cit.aet.artemis.core.test_repository.UserTestRepository; import de.tum.cit.aet.artemis.core.util.CourseUtilService; import de.tum.cit.aet.artemis.core.util.RequestUtilService; +import de.tum.cit.aet.artemis.exercise.domain.ExerciseMode; import de.tum.cit.aet.artemis.exercise.domain.SubmissionType; +import de.tum.cit.aet.artemis.exercise.domain.Team; +import de.tum.cit.aet.artemis.exercise.repository.ExerciseTestRepository; +import de.tum.cit.aet.artemis.exercise.team.TeamUtilService; import de.tum.cit.aet.artemis.exercise.test_repository.ParticipationTestRepository; import de.tum.cit.aet.artemis.exercise.test_repository.SubmissionTestRepository; import de.tum.cit.aet.artemis.lti.service.LtiService; @@ -91,6 +95,9 @@ public class UserTestService { @Autowired private UserUtilService userUtilService; + @Autowired + private TeamUtilService teamUtilService; + @Autowired private CourseUtilService courseUtilService; @@ -128,6 +135,9 @@ public class UserTestService { @Autowired private SubmissionTestRepository submissionRepository; + @Autowired + private ExerciseTestRepository exerciseTestRepository; + public void setup(String testPrefix, MockDelegate mockDelegate) throws Exception { this.TEST_PREFIX = testPrefix; this.mockDelegate = mockDelegate; @@ -915,6 +925,36 @@ public void getAndCreateParticipationVcsAccessToken() throws Exception { participationRepository.deleteById(submission.getParticipation().getId()); } + // Test + public void getAndCreateParticipationVcsAccessTokenForTeamExercise() throws Exception { + User user = userUtilService.getUserByLogin(TEST_PREFIX + "student1"); + var course = courseUtilService.addEmptyCourse(); + var exercise = programmingExerciseUtilService.addProgrammingExerciseToCourse(course); + exercise.setMode(ExerciseMode.TEAM); + exerciseTestRepository.save(exercise); + courseRepository.save(course); + User tutor1 = userTestRepository.findOneByLogin(TEST_PREFIX + "tutor1").orElseThrow(); + Team team = teamUtilService.createTeam(Set.of(user), tutor1, exercise, "team1"); + + var submission = (ProgrammingSubmission) new ProgrammingSubmission().commitHash("abc").type(SubmissionType.MANUAL).submitted(true); + submission = programmingExerciseUtilService.addProgrammingSubmissionToTeamExercise(exercise, submission, team); + + // request existing token + request.get("/api/account/participation-vcs-access-token?participationId=" + submission.getParticipation().getId(), HttpStatus.NOT_FOUND, String.class); + + var token = request.putWithResponseBody("/api/account/participation-vcs-access-token?participationId=" + submission.getParticipation().getId(), null, String.class, + HttpStatus.OK); + assertThat(token).isNotNull(); + + var token2 = request.get("/api/account/participation-vcs-access-token?participationId=" + submission.getParticipation().getId(), HttpStatus.OK, String.class); + assertThat(token2).isEqualTo(token); + + submissionRepository.delete(submission); + participationVCSAccessTokenRepository.deleteAll(); + participationRepository.deleteById(submission.getParticipation().getId()); + teamUtilService.deleteTeam(team); + } + // Test public void createAndDeleteUserVcsAccessToken() throws Exception { User user = userUtilService.getUserByLogin(TEST_PREFIX + "student1"); diff --git a/src/test/java/de/tum/cit/aet/artemis/exercise/participation/ParticipationIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/exercise/participation/ParticipationIntegrationTest.java index 8e97d56893d8..3f32fb13ca5d 100644 --- a/src/test/java/de/tum/cit/aet/artemis/exercise/participation/ParticipationIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/exercise/participation/ParticipationIntegrationTest.java @@ -916,7 +916,8 @@ void getAllParticipationsForExercise_withLatestResults_forQuizExercise() throws courseRepository.save(course); exerciseRepository.save(quizExercise); - var participation = participationUtilService.createAndSaveParticipationForExercise(quizExercise, TEST_PREFIX + "student1"); + final var login = TEST_PREFIX + "student1"; + var participation = participationUtilService.createAndSaveParticipationForExercise(quizExercise, login); var result1 = participationUtilService.createSubmissionAndResult(participation, 42, true); var notGradedResult = participationUtilService.addResultToParticipation(participation, result1.getSubmission()); notGradedResult.setRated(false); @@ -926,11 +927,10 @@ void getAllParticipationsForExercise_withLatestResults_forQuizExercise() throws params.add("withLatestResults", "true"); var participations = request.getList("/api/exercises/" + quizExercise.getId() + "/participations", HttpStatus.OK, StudentParticipation.class, params); - var receivedParticipationWithResult = participations.stream().filter(p -> ((User) p.getParticipant()).getLogin().equals(TEST_PREFIX + "student1")).findFirst() - .orElseThrow(); - assertThat(receivedParticipationWithResult.getResults()).containsOnly(result1); - assertThat(receivedParticipationWithResult.getSubmissions()).isEmpty(); - assertThat(receivedParticipationWithResult.getSubmissionCount()).isEqualTo(1); + var receivedParticipation = participations.stream().filter(p -> p.getParticipantIdentifier().equals(login)).findFirst().orElseThrow(); + assertThat(receivedParticipation.getResults()).containsOnly(notGradedResult); + assertThat(receivedParticipation.getSubmissions()).isEmpty(); + assertThat(receivedParticipation.getSubmissionCount()).isEqualTo(1); } @Test diff --git a/src/test/java/de/tum/cit/aet/artemis/exercise/team/TeamUtilService.java b/src/test/java/de/tum/cit/aet/artemis/exercise/team/TeamUtilService.java index 542647f0dd8d..d24b66221cf8 100644 --- a/src/test/java/de/tum/cit/aet/artemis/exercise/team/TeamUtilService.java +++ b/src/test/java/de/tum/cit/aet/artemis/exercise/team/TeamUtilService.java @@ -244,4 +244,8 @@ public Team createTeam(Set students, User owner, Exercise exercise, String team.setExercise(exercise); return teamRepo.saveAndFlush(team); } + + public void deleteTeam(Team team) { + teamRepo.delete(team); + } } diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseTemplateIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseTemplateIntegrationTest.java index 909814b1e829..ff5faef25c3b 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseTemplateIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseTemplateIntegrationTest.java @@ -2,7 +2,7 @@ import static de.tum.cit.aet.artemis.core.util.TestConstants.COMMIT_HASH_OBJECT_ID; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Fail.fail; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; @@ -83,13 +83,11 @@ class ProgrammingExerciseTemplateIntegrationTest extends AbstractProgrammingInte @BeforeAll static void detectMavenHome() { /* - * Maven invoker only looks for those two values and ignores maven, even if it is available over PATH. Because Maven reports the path when "-version" is used, we use that - * to auto-detect the maven home and store it in the system properties. + * Maven invoker only looks for system properties and ignores maven, even if it is available over PATH. + * Because Maven reports the path when "-version" is used, we use that to auto-detect the maven home and store it in the system properties. */ - String m2Home = System.getenv("M2_HOME"); - String mavenHome = System.getProperty("maven.home"); - if (m2Home != null || mavenHome != null) { + if (isMavenHomeSet()) { return; } @@ -98,57 +96,68 @@ static void detectMavenHome() { var lines = runProcess(new ProcessBuilder(mvnExecutable, "-version")); String prefix = "maven home:"; Optional home = lines.stream().filter(line -> line.toLowerCase().startsWith(prefix)).findFirst(); - if (home.isPresent()) { - System.setProperty("maven.home", home.get().substring(prefix.length()).strip()); - } - else { - fail("maven home not found, unexpected '-version' format"); - } + home.ifPresent(homeLocation -> System.setProperty("maven.home", homeLocation.substring(prefix.length()).strip())); } catch (Exception e) { - fail("maven home not found", e); + log.debug("maven home not found", e); } } + private static boolean isMavenHomeSet() { + String m2Home = System.getenv("M2_HOME"); + String mavenHome = System.getProperty("maven.home"); + + return m2Home != null || mavenHome != null; + } + @BeforeAll static void findAndSetJava17Home() throws Exception { if (Os.isFamily(Os.FAMILY_UNIX) || Os.isFamily(Os.FAMILY_MAC)) { - // Use which to find all java installations on Linux - var javaInstallations = runProcess(new ProcessBuilder("which", "-a", "java")); - for (String path : javaInstallations) { - File binFolder = new File(path).getParentFile(); - if (checkJavaVersion(binFolder, "./java", "-version")) { - return; - } - } + findAndSetJava17UnixSystems(); + } + else if (Os.isFamily(Os.FAMILY_WINDOWS)) { + findAndSetJava17Windows(); + } + } - var alternativeInstallations = runProcess(new ProcessBuilder("/usr/libexec/java_home", "-v", "17")); - for (String path : alternativeInstallations) { - File binFolder = new File(path).getParentFile(); - binFolder = new File(binFolder, "Home/bin"); - if (checkJavaVersion(binFolder, "./java", "-version")) { - return; - } + private static void findAndSetJava17UnixSystems() throws Exception { + // Use which to find all java installations on Linux + var javaInstallations = runProcess(new ProcessBuilder("which", "-a", "java")); + for (String path : javaInstallations) { + File binFolder = new File(path).getParentFile(); + if (checkJavaVersion(binFolder, "./java", "-version")) { + return; } } - else if (Os.isFamily(Os.FAMILY_WINDOWS)) { - // Use PATH to find all java installations on windows - String[] path = System.getenv("PATH").split(";"); - var java17 = Arrays.stream(path).map(Path::of).filter(p -> p.endsWith("bin")).filter(Files::isDirectory).filter(binDir -> Files.exists(binDir.resolve("java.exe"))) - .filter(binDir -> { - try { - return checkJavaVersion(binDir.toFile(), "cmd", "/c", "java.exe", "-version"); - } - catch (Exception e) { - return false; - } - }).findFirst(); - - if (java17.isPresent()) { + + // Mac systems have additional locations where Java could potentially be + if (Os.isFamily(Os.FAMILY_MAC)) { + findAndSetJava17Mac(); + } + } + + private static void findAndSetJava17Mac() throws Exception { + var alternativeInstallations = runProcess(new ProcessBuilder("/usr/libexec/java_home", "-v", "17")); + for (String path : alternativeInstallations) { + File binFolder = new File(path).getParentFile(); + binFolder = new File(binFolder, "Home/bin"); + if (checkJavaVersion(binFolder, "./java", "-version")) { return; } } - fail("Java 17 not found"); + } + + private static void findAndSetJava17Windows() { + // Use PATH to find all java installations on windows + String[] path = System.getenv("PATH").split(";"); + Arrays.stream(path).map(Path::of).filter(p -> p.endsWith("bin")).filter(Files::isDirectory).filter(binDir -> Files.exists(binDir.resolve("java.exe"))).forEach(binDir -> { + try { + checkJavaVersion(binDir.toFile(), "cmd", "/c", "java.exe", "-version"); + } + catch (Exception e) { + // ignore: we still continue to find another Java installation + } + }); } private static boolean checkJavaVersion(File binFolder, String... command) throws Exception { @@ -248,6 +257,7 @@ private Stream languageTypeBuilder() { @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") @MethodSource("languageTypeBuilder") void testTemplateExercise(ProgrammingLanguage language, ProjectType projectType, boolean testwiseCoverageAnalysis) throws Exception { + checkPreconditionsForJavaTemplateExecution(projectType); runTests(language, projectType, exerciseRepo, TestResult.FAILED, testwiseCoverageAnalysis); } @@ -255,9 +265,17 @@ void testTemplateExercise(ProgrammingLanguage language, ProjectType projectType, @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") @MethodSource("languageTypeBuilder") void testTemplateSolution(ProgrammingLanguage language, ProjectType projectType, boolean testwiseCoverageAnalysis) throws Exception { + checkPreconditionsForJavaTemplateExecution(projectType); runTests(language, projectType, solutionRepo, TestResult.SUCCESSFUL, testwiseCoverageAnalysis); } + private void checkPreconditionsForJavaTemplateExecution(final ProjectType projectType) { + if (projectType == null || projectType.isMaven()) { + assumeTrue(isMavenHomeSet(), "Could not find Maven. Skipping execution of template tests."); + } + assumeTrue(java17Home != null, "Could not find Java 17. Skipping execution of template tests."); + } + private void runTests(ProgrammingLanguage language, ProjectType projectType, LocalRepository repository, TestResult testResult, boolean testwiseCoverageAnalysis) throws Exception { exercise.setProgrammingLanguage(language); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseUtilService.java b/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseUtilService.java index e27ce95ac8ca..7178f4376a08 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseUtilService.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseUtilService.java @@ -42,6 +42,7 @@ import de.tum.cit.aet.artemis.exam.util.ExamUtilService; import de.tum.cit.aet.artemis.exercise.domain.InitializationState; import de.tum.cit.aet.artemis.exercise.domain.SubmissionType; +import de.tum.cit.aet.artemis.exercise.domain.Team; import de.tum.cit.aet.artemis.exercise.domain.participation.Participation; import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation; import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationFactory; @@ -741,6 +742,21 @@ public ProgrammingSubmission addProgrammingSubmission(ProgrammingExercise exerci return submission; } + /** + * Adds programming submission to provided programming exercise. The provided login is used to access or create a participation. + * + * @param exercise The exercise to which the submission should be added. + * @param submission The submission which should be added to the programming exercise. + * @param team The login of the user used to access or create an exercise participation. + * @return The created programming submission. + */ + public ProgrammingSubmission addProgrammingSubmissionToTeamExercise(ProgrammingExercise exercise, ProgrammingSubmission submission, Team team) { + StudentParticipation participation = participationUtilService.addTeamParticipationForProgrammingExercise(exercise, team); + submission.setParticipation(participation); + submission = programmingSubmissionRepo.save(submission); + return submission; + } + /** * Adds a submission with a result to the given programming exercise. The submission will be assigned to the corresponding participation of the given login (if exists or * create a new participation). diff --git a/src/test/java/de/tum/cit/aet/artemis/tutorialgroup/TutorialGroupsConfigurationIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/tutorialgroup/TutorialGroupsConfigurationIntegrationTest.java index 0dda38b7f7c6..64c5b0cba89a 100644 --- a/src/test/java/de/tum/cit/aet/artemis/tutorialgroup/TutorialGroupsConfigurationIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/tutorialgroup/TutorialGroupsConfigurationIntegrationTest.java @@ -115,13 +115,8 @@ void getOneOfCourse_asStudent_shouldReturnTutorialGroupsConfiguration() throws E @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void create_asInstructor_shouldCreateTutorialGroupsConfiguration() throws Exception { - // when - var temp = getTutorialGroupsConfigurationPath(courseId); - var temp2 = buildExampleConfiguration(courseId); - var configurationFromRequest = request.postWithResponseBody(getTutorialGroupsConfigurationPath(courseId), buildExampleConfiguration(courseId), TutorialGroupsConfiguration.class, HttpStatus.CREATED); - // then assertThat(configurationFromRequest).isNotNull(); this.assertConfigurationStructure(configurationFromRequest, FIRST_AUGUST_MONDAY, FIRST_SEPTEMBER_MONDAY, courseId, true, true); } @@ -170,7 +165,7 @@ void update_periodChange_deleteTutorialGroupFreePeriodsAndIndividualSessionsAndR * The problem was that jackson tried to deserialize the date and time with the date and time format checkers active, which failed. These checkers * should only be active in a direct create / update case to ensure uuuu-MM-dd format in the database. * - * @throws Exception + * @throws Exception if the request fails */ @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") diff --git a/src/test/javascript/spec/component/detail-overview-list.component.spec.ts b/src/test/javascript/spec/component/detail-overview-list.component.spec.ts index 61e891b11e35..8d5998e3053a 100644 --- a/src/test/javascript/spec/component/detail-overview-list.component.spec.ts +++ b/src/test/javascript/spec/component/detail-overview-list.component.spec.ts @@ -56,7 +56,7 @@ describe('DetailOverviewList', () => { }); it('should initialize and destroy', () => { - component.sections = sections; + fixture.componentRef.setInput('sections', sections); fixture.detectChanges(); expect(component.headlines).toStrictEqual([{ id: 'headline-1', translationKey: 'headline.1' }]); expect(component.headlinesRecord).toStrictEqual({ 'headline.1': 'headline-1' }); @@ -67,7 +67,7 @@ describe('DetailOverviewList', () => { }); it('should escape all falsy values', () => { - component.sections = [ + fixture.componentRef.setInput('sections', [ { headline: 'some-section', details: [ @@ -81,7 +81,7 @@ describe('DetailOverviewList', () => { }, ], }, - ]; + ]); fixture.detectChanges(); const detailListTitleDOMElements = fixture.nativeElement.querySelectorAll('dt[id^=detail-title]'); expect(detailListTitleDOMElements).toHaveLength(1); diff --git a/src/test/javascript/spec/component/lecture-unit/attachment-unit/attachment-unit-form.component.spec.ts b/src/test/javascript/spec/component/lecture-unit/attachment-unit/attachment-unit-form.component.spec.ts index 7235e4bba660..6de2fc97f18e 100644 --- a/src/test/javascript/spec/component/lecture-unit/attachment-unit/attachment-unit-form.component.spec.ts +++ b/src/test/javascript/spec/component/lecture-unit/attachment-unit/attachment-unit-form.component.spec.ts @@ -5,10 +5,11 @@ import { AttachmentUnitFormComponent, AttachmentUnitFormData } from 'app/lecture import { FormDateTimePickerComponent } from 'app/shared/date-time-picker/date-time-picker.component'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import dayjs from 'dayjs/esm'; -import { MockComponent, MockDirective, MockPipe } from 'ng-mocks'; +import { MockComponent, MockDirective, MockModule, MockPipe } from 'ng-mocks'; import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; import { CompetencySelectionComponent } from 'app/shared/competency-selection/competency-selection.component'; import { MAX_FILE_SIZE } from 'app/shared/constants/input.constants'; +import { OwlDateTimeModule, OwlNativeDateTimeModule } from '@danielmoncada/angular-datetime-picker'; describe('AttachmentUnitFormComponent', () => { let attachmentUnitFormComponentFixture: ComponentFixture; @@ -16,11 +17,11 @@ describe('AttachmentUnitFormComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ReactiveFormsModule, FormsModule, MockDirective(NgbTooltip)], + imports: [ReactiveFormsModule, FormsModule, MockDirective(NgbTooltip), MockModule(OwlDateTimeModule), MockModule(OwlNativeDateTimeModule)], declarations: [ AttachmentUnitFormComponent, + FormDateTimePickerComponent, MockPipe(ArtemisTranslatePipe), - MockComponent(FormDateTimePickerComponent), MockComponent(FaIconComponent), MockComponent(CompetencySelectionComponent), ], diff --git a/src/test/javascript/spec/component/lecture-unit/online-unit/online-unit-form.component.spec.ts b/src/test/javascript/spec/component/lecture-unit/online-unit/online-unit-form.component.spec.ts index 8f4411cd4f40..3032bb1cf7a0 100644 --- a/src/test/javascript/spec/component/lecture-unit/online-unit/online-unit-form.component.spec.ts +++ b/src/test/javascript/spec/component/lecture-unit/online-unit/online-unit-form.component.spec.ts @@ -3,7 +3,7 @@ import { OnlineUnitFormComponent, OnlineUnitFormData } from 'app/lecture/lecture import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; -import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponent, MockModule, MockPipe, MockProvider } from 'ng-mocks'; import { FormDateTimePickerComponent } from 'app/shared/date-time-picker/date-time-picker.component'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { OnlineUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/onlineUnit.service'; @@ -11,6 +11,9 @@ import { OnlineResourceDTO } from 'app/lecture/lecture-unit/lecture-unit-managem import { HttpResponse } from '@angular/common/http'; import { of } from 'rxjs'; import { CompetencySelectionComponent } from 'app/shared/competency-selection/competency-selection.component'; +import { OwlDateTimeModule, OwlNativeDateTimeModule } from '@danielmoncada/angular-datetime-picker'; +import { ArtemisTestModule } from '../../../test.module'; +import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; describe('OnlineUnitFormComponent', () => { let onlineUnitFormComponentFixture: ComponentFixture; @@ -18,11 +21,11 @@ describe('OnlineUnitFormComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ReactiveFormsModule, FormsModule], + imports: [ArtemisTestModule, ReactiveFormsModule, FormsModule, MockModule(NgbTooltipModule), MockModule(OwlDateTimeModule), MockModule(OwlNativeDateTimeModule)], declarations: [ OnlineUnitFormComponent, + FormDateTimePickerComponent, MockPipe(ArtemisTranslatePipe), - MockComponent(FormDateTimePickerComponent), MockComponent(FaIconComponent), MockComponent(CompetencySelectionComponent), ], diff --git a/src/test/javascript/spec/component/lecture-unit/text-unit/text-unit-form.component.spec.ts b/src/test/javascript/spec/component/lecture-unit/text-unit/text-unit-form.component.spec.ts index fae301e74331..e54974801b5f 100644 --- a/src/test/javascript/spec/component/lecture-unit/text-unit/text-unit-form.component.spec.ts +++ b/src/test/javascript/spec/component/lecture-unit/text-unit/text-unit-form.component.spec.ts @@ -8,9 +8,12 @@ import { TextUnitFormComponent, TextUnitFormData } from 'app/lecture/lecture-uni import { FormDateTimePickerComponent } from 'app/shared/date-time-picker/date-time-picker.component'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import dayjs from 'dayjs/esm'; -import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponent, MockModule, MockPipe, MockProvider } from 'ng-mocks'; import { MockRouter } from '../../../helpers/mocks/mock-router'; import { CompetencySelectionComponent } from 'app/shared/competency-selection/competency-selection.component'; +import { ArtemisTestModule } from '../../../test.module'; +import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; +import { OwlDateTimeModule, OwlNativeDateTimeModule } from '@danielmoncada/angular-datetime-picker'; @Component({ selector: 'jhi-markdown-editor-monaco', template: '' }) class MarkdownEditorStubComponent { @@ -43,11 +46,11 @@ describe('TextUnitFormComponent', () => { }); TestBed.configureTestingModule({ - imports: [ReactiveFormsModule, FormsModule], + imports: [ArtemisTestModule, ReactiveFormsModule, FormsModule, MockModule(NgbTooltipModule), MockModule(OwlDateTimeModule), MockModule(OwlNativeDateTimeModule)], declarations: [ TextUnitFormComponent, MarkdownEditorStubComponent, - MockComponent(FormDateTimePickerComponent), + FormDateTimePickerComponent, MockPipe(ArtemisTranslatePipe), MockComponent(CompetencySelectionComponent), ], diff --git a/src/test/javascript/spec/component/lecture-unit/video-unit/video-unit-form.component.spec.ts b/src/test/javascript/spec/component/lecture-unit/video-unit/video-unit-form.component.spec.ts index 445163b967ff..84180448c005 100644 --- a/src/test/javascript/spec/component/lecture-unit/video-unit/video-unit-form.component.spec.ts +++ b/src/test/javascript/spec/component/lecture-unit/video-unit/video-unit-form.component.spec.ts @@ -3,10 +3,13 @@ import { VideoUnitFormComponent, VideoUnitFormData } from 'app/lecture/lecture-u import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; -import { MockComponent, MockPipe } from 'ng-mocks'; +import { MockComponent, MockModule, MockPipe } from 'ng-mocks'; import { FormDateTimePickerComponent } from 'app/shared/date-time-picker/date-time-picker.component'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { CompetencySelectionComponent } from 'app/shared/competency-selection/competency-selection.component'; +import { OwlDateTimeModule, OwlNativeDateTimeModule } from '@danielmoncada/angular-datetime-picker'; +import { ArtemisTestModule } from '../../../test.module'; +import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; describe('VideoUnitFormComponent', () => { const validYouTubeUrl = 'https://www.youtube.com/watch?v=8iU8LPEa4o0'; @@ -16,11 +19,11 @@ describe('VideoUnitFormComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ReactiveFormsModule, FormsModule], + imports: [ArtemisTestModule, ReactiveFormsModule, FormsModule, MockModule(NgbTooltipModule), MockModule(OwlDateTimeModule), MockModule(OwlNativeDateTimeModule)], declarations: [ VideoUnitFormComponent, + FormDateTimePickerComponent, MockPipe(ArtemisTranslatePipe), - MockComponent(FormDateTimePickerComponent), MockComponent(FaIconComponent), MockComponent(CompetencySelectionComponent), ], diff --git a/src/test/javascript/spec/component/lecture/lecture-attachments.component.spec.ts b/src/test/javascript/spec/component/lecture/lecture-attachments.component.spec.ts index 72bab398d1f1..83509866f1d9 100644 --- a/src/test/javascript/spec/component/lecture/lecture-attachments.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/lecture-attachments.component.spec.ts @@ -119,7 +119,7 @@ describe('LectureAttachmentsComponent', () => { }); it('should load existing lecture', fakeAsync(() => { - comp.lectureId = 42; + fixture.componentRef.setInput('lectureId', 42); const findWithDetailsStub = jest.spyOn(lectureService, 'findWithDetails').mockReturnValue( of( new HttpResponse({ diff --git a/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard-units.component.spec.ts b/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard-units.component.spec.ts index 6abf0ee067bd..c850a83f6ecc 100644 --- a/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard-units.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard-units.component.spec.ts @@ -95,7 +95,7 @@ describe('LectureWizardUnitComponent', () => { unitCreationCard.onUnitCreationCardClicked.emit(LectureUnitType.VIDEO); wizardUnitComponentFixture.whenStable().then(() => { - expect(wizardUnitComponent.isVideoUnitFormOpen).toBeTrue(); + expect(wizardUnitComponent.isVideoUnitFormOpen()).toBeTrue(); }); })); @@ -106,7 +106,7 @@ describe('LectureWizardUnitComponent', () => { unitCreationCard.onUnitCreationCardClicked.emit(LectureUnitType.ONLINE); wizardUnitComponentFixture.whenStable().then(() => { - expect(wizardUnitComponent.isOnlineUnitFormOpen).toBeTrue(); + expect(wizardUnitComponent.isOnlineUnitFormOpen()).toBeTrue(); }); })); @@ -117,7 +117,7 @@ describe('LectureWizardUnitComponent', () => { unitCreationCard.onUnitCreationCardClicked.emit(LectureUnitType.ATTACHMENT); wizardUnitComponentFixture.whenStable().then(() => { - expect(wizardUnitComponent.isAttachmentUnitFormOpen).toBeTrue(); + expect(wizardUnitComponent.isAttachmentUnitFormOpen()).toBeTrue(); }); })); @@ -128,7 +128,7 @@ describe('LectureWizardUnitComponent', () => { unitCreationCard.onUnitCreationCardClicked.emit(LectureUnitType.TEXT); wizardUnitComponentFixture.whenStable().then(() => { - expect(wizardUnitComponent.isTextUnitFormOpen).toBeTrue(); + expect(wizardUnitComponent.isTextUnitFormOpen()).toBeTrue(); }); })); @@ -139,7 +139,7 @@ describe('LectureWizardUnitComponent', () => { unitCreationCard.onUnitCreationCardClicked.emit(LectureUnitType.EXERCISE); wizardUnitComponentFixture.whenStable().then(() => { - expect(wizardUnitComponent.isExerciseUnitFormOpen).toBeTrue(); + expect(wizardUnitComponent.isExerciseUnitFormOpen()).toBeTrue(); }); })); @@ -149,11 +149,11 @@ describe('LectureWizardUnitComponent', () => { wizardUnitComponent.onCloseLectureUnitForms(); wizardUnitComponentFixture.whenStable().then(() => { - expect(wizardUnitComponent.isOnlineUnitFormOpen).toBeFalse(); - expect(wizardUnitComponent.isTextUnitFormOpen).toBeFalse(); - expect(wizardUnitComponent.isExerciseUnitFormOpen).toBeFalse(); - expect(wizardUnitComponent.isAttachmentUnitFormOpen).toBeFalse(); - expect(wizardUnitComponent.isVideoUnitFormOpen).toBeFalse(); + expect(wizardUnitComponent.isOnlineUnitFormOpen()).toBeFalse(); + expect(wizardUnitComponent.isTextUnitFormOpen()).toBeFalse(); + expect(wizardUnitComponent.isExerciseUnitFormOpen()).toBeFalse(); + expect(wizardUnitComponent.isAttachmentUnitFormOpen()).toBeFalse(); + expect(wizardUnitComponent.isVideoUnitFormOpen()).toBeFalse(); }); })); @@ -194,7 +194,7 @@ describe('LectureWizardUnitComponent', () => { const updateSpy = jest.spyOn(wizardUnitComponent.unitManagementComponent, 'loadData'); - wizardUnitComponent.isVideoUnitFormOpen = true; + wizardUnitComponent.isVideoUnitFormOpen.set(true); wizardUnitComponent.createEditVideoUnit(formData); @@ -226,7 +226,7 @@ describe('LectureWizardUnitComponent', () => { wizardUnitComponentFixture.detectChanges(); tick(); - wizardUnitComponent.isVideoUnitFormOpen = true; + wizardUnitComponent.isVideoUnitFormOpen.set(true); wizardUnitComponent.createEditVideoUnit(formData); @@ -277,7 +277,7 @@ describe('LectureWizardUnitComponent', () => { const updateSpy = jest.spyOn(wizardUnitComponent.unitManagementComponent, 'loadData'); - wizardUnitComponent.isTextUnitFormOpen = true; + wizardUnitComponent.isTextUnitFormOpen.set(true); wizardUnitComponent.createEditTextUnit(formData); @@ -308,7 +308,7 @@ describe('LectureWizardUnitComponent', () => { wizardUnitComponentFixture.detectChanges(); tick(); - wizardUnitComponent.isTextUnitFormOpen = true; + wizardUnitComponent.isTextUnitFormOpen.set(true); wizardUnitComponent.createEditTextUnit(formData); @@ -333,7 +333,7 @@ describe('LectureWizardUnitComponent', () => { wizardUnitComponentFixture.detectChanges(); tick(); - wizardUnitComponent.isTextUnitFormOpen = true; + wizardUnitComponent.isTextUnitFormOpen.set(true); wizardUnitComponent.createEditTextUnit(formData); @@ -360,7 +360,7 @@ describe('LectureWizardUnitComponent', () => { wizardUnitComponentFixture.detectChanges(); tick(); - wizardUnitComponent.isVideoUnitFormOpen = true; + wizardUnitComponent.isVideoUnitFormOpen.set(true); wizardUnitComponent.createEditVideoUnit(formData); @@ -387,7 +387,7 @@ describe('LectureWizardUnitComponent', () => { wizardUnitComponentFixture.detectChanges(); tick(); - wizardUnitComponent.isOnlineUnitFormOpen = true; + wizardUnitComponent.isOnlineUnitFormOpen.set(true); wizardUnitComponent.createEditOnlineUnit(formData); @@ -434,7 +434,7 @@ describe('LectureWizardUnitComponent', () => { const updateSpy = jest.spyOn(wizardUnitComponent.unitManagementComponent, 'loadData'); - wizardUnitComponent.isOnlineUnitFormOpen = true; + wizardUnitComponent.isOnlineUnitFormOpen.set(true); wizardUnitComponent.createEditOnlineUnit(formDate); @@ -466,7 +466,7 @@ describe('LectureWizardUnitComponent', () => { wizardUnitComponentFixture.detectChanges(); tick(); - wizardUnitComponent.isOnlineUnitFormOpen = true; + wizardUnitComponent.isOnlineUnitFormOpen.set(true); wizardUnitComponent.createEditOnlineUnit(formData); @@ -538,7 +538,7 @@ describe('LectureWizardUnitComponent', () => { const updateSpy = jest.spyOn(wizardUnitComponent.unitManagementComponent, 'loadData'); - wizardUnitComponent.isAttachmentUnitFormOpen = true; + wizardUnitComponent.isAttachmentUnitFormOpen.set(true); wizardUnitComponent.createEditAttachmentUnit(attachmentUnitFormData); @@ -607,7 +607,7 @@ describe('LectureWizardUnitComponent', () => { wizardUnitComponent.isEditingLectureUnit = true; wizardUnitComponent.currentlyProcessedAttachmentUnit = new AttachmentUnit(); wizardUnitComponent.currentlyProcessedAttachmentUnit.attachment = new Attachment(); - wizardUnitComponent.isAttachmentUnitFormOpen = true; + wizardUnitComponent.isAttachmentUnitFormOpen.set(true); wizardUnitComponent.createEditAttachmentUnit(attachmentUnitFormData); @@ -663,7 +663,7 @@ describe('LectureWizardUnitComponent', () => { wizardUnitComponentFixture.detectChanges(); tick(); - wizardUnitComponent.isAttachmentUnitFormOpen = true; + wizardUnitComponent.isAttachmentUnitFormOpen.set(true); wizardUnitComponent.createEditAttachmentUnit(attachmentUnitFormData); @@ -719,7 +719,7 @@ describe('LectureWizardUnitComponent', () => { wizardUnitComponentFixture.detectChanges(); tick(); - wizardUnitComponent.isAttachmentUnitFormOpen = true; + wizardUnitComponent.isAttachmentUnitFormOpen.set(true); wizardUnitComponent.createEditAttachmentUnit(attachmentUnitFormData); @@ -742,7 +742,7 @@ describe('LectureWizardUnitComponent', () => { wizardUnitComponentFixture.detectChanges(); tick(); - wizardUnitComponent.isAttachmentUnitFormOpen = true; + wizardUnitComponent.isAttachmentUnitFormOpen.set(true); wizardUnitComponent.createEditAttachmentUnit(formData); @@ -839,6 +839,6 @@ describe('LectureWizardUnitComponent', () => { wizardUnitComponentFixture.detectChanges(); expect(wizardUnitComponent).not.toBeNull(); - expect(wizardUnitComponent.isExerciseUnitFormOpen).toBeTrue(); + expect(wizardUnitComponent.isExerciseUnitFormOpen()).toBeTrue(); })); }); diff --git a/src/test/javascript/spec/component/participation/participation.component.spec.ts b/src/test/javascript/spec/component/participation/participation.component.spec.ts index 6661b643ed31..62f43f1f2013 100644 --- a/src/test/javascript/spec/component/participation/participation.component.spec.ts +++ b/src/test/javascript/spec/component/participation/participation.component.spec.ts @@ -125,7 +125,7 @@ describe('ParticipationComponent', () => { expect(exerciseFindStub).toHaveBeenCalledOnce(); expect(exerciseFindStub).toHaveBeenCalledWith(theExercise.id); expect(participationFindStub).toHaveBeenCalledOnce(); - expect(participationFindStub).toHaveBeenCalledWith(theExercise.id, true); + expect(participationFindStub).toHaveBeenCalledWith(theExercise.id, false); })); it('should initialize for programming exercise', fakeAsync(() => { @@ -154,7 +154,7 @@ describe('ParticipationComponent', () => { expect(exerciseFindStub).toHaveBeenCalledOnce(); expect(exerciseFindStub).toHaveBeenCalledWith(theExercise.id); expect(participationFindStub).toHaveBeenCalledOnce(); - expect(participationFindStub).toHaveBeenCalledWith(theExercise.id, true); + expect(participationFindStub).toHaveBeenCalledWith(theExercise.id, false); expect(submissionGetStateStub).toHaveBeenCalledOnce(); expect(submissionGetStateStub).toHaveBeenCalledWith(theExercise.id); })); diff --git a/src/test/javascript/spec/component/programming-exercise/programming-exercise-detail.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/programming-exercise-detail.component.spec.ts index 8563723b42b8..f355ba91a806 100644 --- a/src/test/javascript/spec/component/programming-exercise/programming-exercise-detail.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/programming-exercise-detail.component.spec.ts @@ -22,9 +22,7 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { MockSyncStorage } from '../../helpers/mocks/service/mock-sync-storage.service'; import { LocalStorageService, SessionStorageService } from 'ngx-webstorage'; import { MockProgrammingExerciseGradingService } from '../../helpers/mocks/service/mock-programming-exercise-grading.service'; -import { ProgrammingExerciseGitDiffReport } from 'app/entities/hestia/programming-exercise-git-diff-report.model'; import { ProgrammingExerciseSolutionEntry } from 'app/entities/hestia/programming-exercise-solution-entry.model'; -import { BuildLogStatisticsDTO } from 'app/entities/programming/build-log-statistics-dto'; import { TemplateProgrammingExerciseParticipation } from 'app/entities/participation/template-programming-exercise-participation.model'; import { SolutionProgrammingExerciseParticipation } from 'app/entities/participation/solution-programming-exercise-participation.model'; import { HttpResponse } from '@angular/common/http'; @@ -34,17 +32,24 @@ import { ProgrammingLanguageFeatureService, } from 'app/exercises/programming/shared/service/programming-language-feature/programming-language-feature.service'; import { MockRouter } from '../../helpers/mocks/mock-router'; +import { BuildConfig } from '../../../../../main/webapp/app/entities/programming/build-config.model'; +import { ProgrammingExerciseGitDiffReport } from 'app/entities/hestia/programming-exercise-git-diff-report.model'; +import { BuildLogStatisticsDTO } from 'app/entities/programming/build-log-statistics-dto'; +import { SubmissionPolicyService } from '../../../../../main/webapp/app/exercises/programming/manage/services/submission-policy.service'; -describe('ProgrammingExercise Management Detail Component', () => { +describe('ProgrammingExerciseDetailComponent', () => { let comp: ProgrammingExerciseDetailComponent; let fixture: ComponentFixture; let statisticsService: StatisticsService; let exerciseService: ProgrammingExerciseService; let alertService: AlertService; let profileService: ProfileService; + let submissionPolicyService: SubmissionPolicyService; let programmingLanguageFeatureService: ProgrammingLanguageFeatureService; let statisticsServiceStub: jest.SpyInstance; let gitDiffReportStub: jest.SpyInstance; + let profileServiceStub: jest.SpyInstance; + let submissionPolicyServiceStub: jest.SpyInstance; let buildLogStatisticsStub: jest.SpyInstance; let findWithTemplateAndSolutionParticipationStub: jest.SpyInstance; let router: Router; @@ -59,6 +64,9 @@ describe('ProgrammingExercise Management Detail Component', () => { solutionParticipation: { id: 2, } as SolutionProgrammingExerciseParticipation, + buildConfig: { + testwiseCoverageEnabled: true, + } as BuildConfig, } as ProgrammingExercise; const exerciseStatistics = { @@ -128,6 +136,8 @@ describe('ProgrammingExercise Management Detail Component', () => { alertService = fixture.debugElement.injector.get(AlertService); exerciseService = fixture.debugElement.injector.get(ProgrammingExerciseService); profileService = fixture.debugElement.injector.get(ProfileService); + submissionPolicyService = fixture.debugElement.injector.get(SubmissionPolicyService); + programmingLanguageFeatureService = fixture.debugElement.injector.get(ProgrammingLanguageFeatureService); router = fixture.debugElement.injector.get(Router); modalService = fixture.debugElement.injector.get(NgbModal); @@ -136,6 +146,8 @@ describe('ProgrammingExercise Management Detail Component', () => { .spyOn(exerciseService, 'findWithTemplateAndSolutionParticipationAndLatestResults') .mockReturnValue(of(new HttpResponse({ body: mockProgrammingExercise }))); gitDiffReportStub = jest.spyOn(exerciseService, 'getDiffReport').mockReturnValue(of(gitDiffReport)); + profileServiceStub = jest.spyOn(profileService, 'getProfileInfo').mockReturnValue(of(profileInfo)); + submissionPolicyServiceStub = jest.spyOn(submissionPolicyService, 'getSubmissionPolicyOfProgrammingExercise').mockReturnValue(of(undefined)); buildLogStatisticsStub = jest.spyOn(exerciseService, 'getBuildLogStatistics').mockReturnValue(of(buildLogStatistics)); jest.spyOn(profileService, 'getProfileInfo').mockReturnValue(of(profileInfo)); @@ -148,6 +160,19 @@ describe('ProgrammingExercise Management Detail Component', () => { jest.restoreAllMocks(); }); + it('should reload on participation change', fakeAsync(() => { + const loadDiffSpy = jest.spyOn(comp, 'loadGitDiffReport'); + jest.spyOn(exerciseService, 'getLatestResult').mockReturnValue({ successful: true }); + jest.spyOn(exerciseService, 'getLatestFullTestwiseCoverageReport').mockReturnValue(of({ coveredLineRatio: 0.5 })); + comp.programmingExercise = mockProgrammingExercise; + comp.programmingExerciseBuildConfig = mockProgrammingExercise.buildConfig; + comp.onParticipationChange(); + tick(); + expect(loadDiffSpy).toHaveBeenCalledOnce(); + expect(gitDiffReportStub).toHaveBeenCalledOnce(); + expect(comp.programmingExercise.coveredLinesRatio).toBe(0.5); + })); + describe('onInit for course exercise', () => { const programmingExercise = new ProgrammingExercise(new Course(), undefined); programmingExercise.id = 123; @@ -163,6 +188,8 @@ describe('ProgrammingExercise Management Detail Component', () => { // THEN expect(findWithTemplateAndSolutionParticipationStub).toHaveBeenCalledOnce(); + expect(profileServiceStub).toHaveBeenCalledTimes(2); + expect(submissionPolicyServiceStub).toHaveBeenCalledOnce(); expect(gitDiffReportStub).toHaveBeenCalledOnce(); expect(statisticsServiceStub).toHaveBeenCalledOnce(); await Promise.resolve(); @@ -285,18 +312,6 @@ describe('ProgrammingExercise Management Detail Component', () => { expect(comp.isBuildPlanEditable).toBe(editable); }); - it('should reload on participation change', fakeAsync(() => { - const loadDiffSpy = jest.spyOn(comp, 'loadGitDiffReport'); - jest.spyOn(exerciseService, 'getLatestResult').mockReturnValue({ successful: true }); - jest.spyOn(exerciseService, 'getLatestFullTestwiseCoverageReport').mockReturnValue(of({ coveredLineRatio: 0.5 })); - comp.programmingExercise = mockProgrammingExercise; - comp.programmingExercise.buildConfig!.testwiseCoverageEnabled = true; - comp.onParticipationChange(); - tick(); - expect(loadDiffSpy).toHaveBeenCalledOnce(); - expect(comp.programmingExercise.coveredLinesRatio).toBe(0.5); - })); - it('should combine template commit', () => { const combineCommitsSpy = jest.spyOn(exerciseService, 'combineTemplateRepositoryCommits').mockReturnValue(of(new HttpResponse({ body: null }))); const successSpy = jest.spyOn(alertService, 'success'); diff --git a/src/test/javascript/spec/service/metis/post.service.spec.ts b/src/test/javascript/spec/service/metis/post.service.spec.ts index dae7803858b5..7a9de630d53f 100644 --- a/src/test/javascript/spec/service/metis/post.service.spec.ts +++ b/src/test/javascript/spec/service/metis/post.service.spec.ts @@ -4,7 +4,7 @@ import { take } from 'rxjs/operators'; import { Post } from 'app/entities/metis/post.model'; import { PostService } from 'app/shared/metis/post.service'; import { DisplayPriority } from 'app/shared/metis/metis.util'; -import { metisCourse, metisCoursePosts, metisPostExerciseUser1, metisPostToCreateUser1, metisTags } from '../../helpers/sample/metis-sample-data'; +import { metisCourse, metisCoursePosts, metisPostExerciseUser1, metisPostToCreateUser1 } from '../../helpers/sample/metis-sample-data'; import { provideHttpClient } from '@angular/common/http'; describe('Post Service', () => { @@ -33,18 +33,6 @@ describe('Post Service', () => { tick(); })); - it('should return all similar posts in a course', fakeAsync(() => { - const returnedFromService = metisCoursePosts.slice(0, 4); - const expected = returnedFromService; - service - .computeSimilarityScoresWithCoursePosts(metisPostExerciseUser1, metisCourse.id!) - .pipe(take(2)) - .subscribe((resp) => expect(resp.body).toEqual(expected)); - const req = httpMock.expectOne({ method: 'POST' }); - req.flush(returnedFromService); - tick(); - })); - it('should update a Post', fakeAsync(() => { const returnedFromService = { ...metisPostExerciseUser1, content: 'This is another test post' }; const expected = { ...returnedFromService }; @@ -102,18 +90,6 @@ describe('Post Service', () => { tick(); })); - it('should return all post tags for a course', fakeAsync(() => { - const returnedFromService = metisTags; - const expected = returnedFromService; - service - .getAllPostTagsByCourseId(metisCourse.id!) - .pipe(take(2)) - .subscribe((resp) => expect(resp.body).toEqual(expected)); - const req = httpMock.expectOne({ method: 'GET' }); - req.flush(returnedFromService); - tick(); - })); - it('should use /posts endpoints if plagiarismCaseId is provided in the postContextFilter', fakeAsync(() => { const plagiarismCaseId = 123; const expectedUrl = `${service.resourceUrl}${metisCourse.id}/posts?plagiarismCaseId=${plagiarismCaseId}`;