From 8b3c4edd45255e57785d6467e142487fc9520642 Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Sat, 22 Feb 2025 21:59:29 +0100 Subject: [PATCH 01/17] Development: Update node and npm version (#10387) --- docs/dev/setup.rst | 4 ++-- docs/dev/setup/client.rst | 2 +- gradle.properties | 4 ++-- package-lock.json | 3 ++- package.json | 3 ++- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/dev/setup.rst b/docs/dev/setup.rst index dc595432ce10..8c4a92c48735 100644 --- a/docs/dev/setup.rst +++ b/docs/dev/setup.rst @@ -32,10 +32,10 @@ following dependencies/tools on your machine: 2. `MySQL Database Server 9 `__, or `PostgreSQL 17 `_: Artemis uses Hibernate to store entities in an SQL database and Liquibase to automatically apply schema transformations when updating Artemis. -3. `Node.js `__: We use Node LTS (>=22.10.0 < 23) to compile +3. `node `__: We use node LTS (>=22.14.0 < 23) to compile and run the client Angular application. Depending on your system, you can install Node either from source or as a pre-packaged bundle. -4. `Npm `__: We use Npm (>=10.8.0) to +4. `npm `__: We use npm (>=11.1.0) to manage client side dependencies. Npm is typically bundled with Node.js, but can also be installed separately. 5. ( `Graphviz `__: We use Graphviz to generate graphs within exercise task diff --git a/docs/dev/setup/client.rst b/docs/dev/setup/client.rst index 9f89091ebb10..8e7cd029b07f 100644 --- a/docs/dev/setup/client.rst +++ b/docs/dev/setup/client.rst @@ -43,7 +43,7 @@ with your TUM Online account. .. HINT:: In case you encounter any problems regarding JavaScript heap memory leaks when executing ``npm run start`` or any other scripts from ``package.json``, you can adjust a - `memory limit parameter `__ + `memory limit parameter `__ (``node-options=--max-old-space-size=6144``) which is set by default in the project-wide `.npmrc` file. If you still face the issue, you can try to set a lower/higher value than 6144 MB. diff --git a/gradle.properties b/gradle.properties index 1debabecafc9..e8840e1178a2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,8 +2,8 @@ rootProject.name=Artemis profile=dev # Build properties -node_version=22.12.0 -npm_version=10.9.0 +node_version=22.14.0 +npm_version=11.1.0 # Dependency versions jhipster_dependencies_version=8.9.0 diff --git a/package-lock.json b/package-lock.json index c3fefedb3051..ce911ef41fd1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -143,7 +143,8 @@ "weak-napi": "2.0.2" }, "engines": { - "node": ">=22.10.0" + "node": ">=22.14.0", + "npm": ">=11.1.0" } }, "node_modules/@ampproject/remapping": { diff --git a/package.json b/package.json index 70be2dea1ecc..a8f011be5570 100644 --- a/package.json +++ b/package.json @@ -193,7 +193,8 @@ "weak-napi": "2.0.2" }, "engines": { - "node": ">=22.10.0" + "node": ">=22.14.0", + "npm": ">=11.1.0" }, "scripts": { "build": "npm run webapp:prod --", From cc09e992ba3d405a5a5d824525ede9fdceb086f8 Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Mon, 24 Feb 2025 08:54:29 +0100 Subject: [PATCH 02/17] Development: Implement small server improvements --- .../core/domain/AbstractAuditingEntity.java | 12 --------- .../artemis/core/service/CourseService.java | 6 ----- .../exam/domain/event/ExamLiveEvent.java | 10 ++----- .../exam/service/ExamLiveEventsService.java | 26 ++++++------------- .../aet/artemis/exam/service/ExamService.java | 2 +- .../aet/artemis/exam/web/ExamResource.java | 6 ++--- .../artemis/exam/web/StudentExamResource.java | 11 +++----- .../exercise/repository/TeamRepository.java | 6 ----- .../exercise/service/ExerciseService.java | 2 +- .../web/PlagiarismResultResponseBuilder.java | 17 +----------- .../exam/StudentExamIntegrationTest.java | 4 +-- .../artemis/exercise/team/TeamFactory.java | 4 --- .../team/TeamImportIntegrationTest.java | 5 ++-- .../exercise/team/TeamUtilService.java | 4 --- .../FileUploadExerciseIntegrationTest.java | 2 +- .../ModelingExerciseIntegrationTest.java | 5 ++-- .../PlagiarismResultResponseBuilderTest.java | 9 +++---- .../text/TextExerciseIntegrationTest.java | 8 +++--- 18 files changed, 35 insertions(+), 104 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/domain/AbstractAuditingEntity.java b/src/main/java/de/tum/cit/aet/artemis/core/domain/AbstractAuditingEntity.java index 053dd084a3b2..7b18e01d5fe3 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/domain/AbstractAuditingEntity.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/domain/AbstractAuditingEntity.java @@ -46,10 +46,6 @@ public String getCreatedBy() { return createdBy; } - public void setCreatedBy(String createdBy) { - this.createdBy = createdBy; - } - public Instant getCreatedDate() { return createdDate; } @@ -62,15 +58,7 @@ public String getLastModifiedBy() { return lastModifiedBy; } - public void setLastModifiedBy(String lastModifiedBy) { - this.lastModifiedBy = lastModifiedBy; - } - public Instant getLastModifiedDate() { return lastModifiedDate; } - - public void setLastModifiedDate(Instant lastModifiedDate) { - this.lastModifiedDate = lastModifiedDate; - } } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java index 6fa57bb32487..af95bb866d9f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java @@ -1044,9 +1044,6 @@ public ResponseEntity> getAllUsersInGroup(Course course, String groupN user.setActivationKey(null); user.setLangKey(null); user.setLastNotificationRead(null); - user.setLastModifiedBy(null); - user.setLastModifiedDate(null); - user.setCreatedBy(null); user.setCreatedDate(null); }); removeUserVariables(usersInGroup); @@ -1201,9 +1198,6 @@ private void removeUserVariables(Iterable usersInGroup) { user.setActivationKey(null); user.setLangKey(null); user.setLastNotificationRead(null); - user.setLastModifiedBy(null); - user.setLastModifiedDate(null); - user.setCreatedBy(null); user.setCreatedDate(null); }); } diff --git a/src/main/java/de/tum/cit/aet/artemis/exam/domain/event/ExamLiveEvent.java b/src/main/java/de/tum/cit/aet/artemis/exam/domain/event/ExamLiveEvent.java index 0237a1bcd69a..5ae59d720cf0 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exam/domain/event/ExamLiveEvent.java +++ b/src/main/java/de/tum/cit/aet/artemis/exam/domain/event/ExamLiveEvent.java @@ -13,6 +13,7 @@ import org.hibernate.annotations.Cache; import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @@ -36,6 +37,7 @@ @EntityListeners(AuditingEntityListener.class) public abstract class ExamLiveEvent extends DomainObject { + @CreatedBy @Column(name = "created_by", nullable = false, length = 50, updatable = false) private String createdBy; @@ -53,18 +55,10 @@ public String getCreatedBy() { return createdBy; } - public void setCreatedBy(String createdBy) { - this.createdBy = createdBy; - } - public Instant getCreatedDate() { return createdDate; } - public void setCreatedDate(Instant createdDate) { - this.createdDate = createdDate; - } - public Long getExamId() { return examId; } 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 31ee3dbf9e80..3bd100e278cd 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 @@ -7,7 +7,6 @@ import org.springframework.stereotype.Service; import de.tum.cit.aet.artemis.communication.service.WebsocketMessagingService; -import de.tum.cit.aet.artemis.core.domain.User; 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; @@ -66,15 +65,13 @@ public ExamLiveEventsService(WebsocketMessagingService websocketMessagingService * * @param exam the exam to send the announcement to. * @param message The message to send. - * @param sentBy The user who sent the message. * @return The created event. */ - public ExamWideAnnouncementEvent createAndDistributeExamAnnouncementEvent(Exam exam, String message, User sentBy) { + public ExamWideAnnouncementEvent createAndDistributeExamAnnouncementEvent(Exam exam, String message) { var event = new ExamWideAnnouncementEvent(); // Common fields event.setExamId(exam.getId()); - event.setCreatedBy(sentBy.getName()); // Specific fields event.setTextContent(message); @@ -87,16 +84,14 @@ public ExamWideAnnouncementEvent createAndDistributeExamAnnouncementEvent(Exam e * * @param studentExam The student exam the where the popup should be shown * @param message The message to send. - * @param sentBy The user who sent the message. * @return The created event. */ - public ExamAttendanceCheckEvent createAndSendExamAttendanceCheckEvent(StudentExam studentExam, String message, User sentBy) { + public ExamAttendanceCheckEvent createAndSendExamAttendanceCheckEvent(StudentExam studentExam, String message) { var event = new ExamAttendanceCheckEvent(); // Common fields event.setExamId(studentExam.getExam().getId()); event.setStudentExamId(studentExam.getId()); - event.setCreatedBy(sentBy.getName()); // specific fields event.setTextContent(message); @@ -111,15 +106,13 @@ public ExamAttendanceCheckEvent createAndSendExamAttendanceCheckEvent(StudentExa * @param newWorkingTime The new working time in seconds * @param oldWorkingTime The old working time in seconds * @param courseWide set to true if this event is caused by a course wide update that affects all students; false otherwise - * @param sentBy The user who performed the update */ - public void createAndSendWorkingTimeUpdateEvent(StudentExam studentExam, int newWorkingTime, int oldWorkingTime, boolean courseWide, User sentBy) { + public void createAndSendWorkingTimeUpdateEvent(StudentExam studentExam, int newWorkingTime, int oldWorkingTime, boolean courseWide) { var event = new WorkingTimeUpdateEvent(); // Common fields event.setExamId(studentExam.getExam().getId()); event.setStudentExamId(studentExam.getId()); - event.setCreatedBy(sentBy.getName()); // Specific fields event.setNewWorkingTime(newWorkingTime); @@ -132,15 +125,14 @@ public void createAndSendWorkingTimeUpdateEvent(StudentExam studentExam, int new /** * Send a problem statement update to all affected students. * - * @param exercise The exam exercise the problem statement was updated for - * @param message The message to send - * @param instructor The user who performed the update + * @param exercise The exam exercise the problem statement was updated for + * @param message The message to send */ @Async - public void createAndSendProblemStatementUpdateEvent(Exercise exercise, String message, User instructor) { + public void createAndSendProblemStatementUpdateEvent(Exercise exercise, String message) { Exam exam = exercise.getExam(); studentExamRepository.findAllWithExercisesByExamId(exam.getId()).stream().filter(studentExam -> studentExam.getExercises().contains(exercise)) - .forEach(studentExam -> this.createAndSendProblemStatementUpdateEvent(studentExam, exercise, message, instructor)); + .forEach(studentExam -> this.createAndSendProblemStatementUpdateEvent(studentExam, exercise, message)); } /** @@ -149,15 +141,13 @@ public void createAndSendProblemStatementUpdateEvent(Exercise exercise, String m * @param studentExam The student exam containing the exercise with updated problem statement * @param exercise The updated exercise * @param message The message to send - * @param sentBy The user who performed the update */ - public void createAndSendProblemStatementUpdateEvent(StudentExam studentExam, Exercise exercise, String message, User sentBy) { + public void createAndSendProblemStatementUpdateEvent(StudentExam studentExam, Exercise exercise, String message) { var event = new ProblemStatementUpdateEvent(); // Common fields event.setExamId(studentExam.getExam().getId()); event.setStudentExamId(studentExam.getId()); - event.setCreatedBy(sentBy.getName()); // Specific fields event.setTextContent(message); diff --git a/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamService.java b/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamService.java index fe337ea23d5f..e891c8aadf0c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamService.java +++ b/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamService.java @@ -1447,7 +1447,7 @@ public void updateStudentExamsAndRescheduleExercises(Exam exam, int originalExam // NOTE: if the exam is already visible, notify the student about the working time change if (now.isAfter(exam.getVisibleDate())) { - examLiveEventsService.createAndSendWorkingTimeUpdateEvent(studentExam, studentExam.getWorkingTime(), originalStudentWorkingTime, true, instructor); + examLiveEventsService.createAndSendWorkingTimeUpdateEvent(studentExam, studentExam.getWorkingTime(), originalStudentWorkingTime, true); } } studentExamRepository.saveAll(studentExams); diff --git a/src/main/java/de/tum/cit/aet/artemis/exam/web/ExamResource.java b/src/main/java/de/tum/cit/aet/artemis/exam/web/ExamResource.java index 510d28208eca..ac2c4383d756 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exam/web/ExamResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/exam/web/ExamResource.java @@ -76,7 +76,6 @@ import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastInstructor; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastTutor; -import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInCourse.EnforceAtLeastInstructorInCourse; import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; import de.tum.cit.aet.artemis.core.service.feature.Feature; import de.tum.cit.aet.artemis.core.service.feature.FeatureToggle; @@ -361,7 +360,7 @@ public ResponseEntity createExamAnnouncement(@Path throw new BadRequestAlertException("Exam is not visible to students", "exam", "examNotVisible"); } - var event = examLiveEventsService.createAndDistributeExamAnnouncementEvent(exam, message, userRepository.getUser()); + var event = examLiveEventsService.createAndDistributeExamAnnouncementEvent(exam, message); log.debug("createExamAnnouncement took {} for exam {}", TimeLogUtil.formatDurationFrom(start), examId); return ResponseEntity.ok(event.asDTO()); } @@ -1335,8 +1334,9 @@ public ResponseEntity> getAllSuspiciousExamSessio * @return the ResponseEntity with status 200 (OK) and with body a summary of the deletion of the exam */ @GetMapping("courses/{courseId}/exams/{examId}/deletion-summary") - @EnforceAtLeastInstructorInCourse + @EnforceAtLeastInstructor public ResponseEntity getDeletionSummary(@PathVariable long courseId, @PathVariable long examId) { + examAccessService.checkCourseAndExamAccessForInstructorElseThrow(courseId, examId); log.debug("REST request to get deletion summary for exam : {}", examId); return ResponseEntity.ok(examDeletionService.getExamDeletionSummary(examId)); } diff --git a/src/main/java/de/tum/cit/aet/artemis/exam/web/StudentExamResource.java b/src/main/java/de/tum/cit/aet/artemis/exam/web/StudentExamResource.java index 181b6b8205b1..5d721102c488 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exam/web/StudentExamResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/exam/web/StudentExamResource.java @@ -124,8 +124,6 @@ public class StudentExamResource { private final ExamLiveEventRepository examLiveEventRepository; - private static final boolean IS_TEST_RUN = false; - @Value("${info.student-exam-store-session-data:#{true}}") private boolean storeSessionDataInStudentExamSession; @@ -251,7 +249,7 @@ public ResponseEntity updateWorkingTime(@PathVariable Long courseId Exam exam = examService.findByIdWithExerciseGroupsAndExercisesElseThrow(examId, false); if (now.isAfter(exam.getVisibleDate())) { instanceMessageSendService.sendStudentExamIndividualWorkingTimeChangeDuringConduction(studentExamId); - examLiveEventsService.createAndSendWorkingTimeUpdateEvent(savedStudentExam, workingTime, originalWorkingTime, false, userRepository.getUser()); + examLiveEventsService.createAndSendWorkingTimeUpdateEvent(savedStudentExam, workingTime, originalWorkingTime, false); } if (now.isBefore(examDateService.getLatestIndividualExamEndDate(exam))) { // potentially re-schedule clustering of modeling submissions (in case Compass is active) @@ -279,7 +277,7 @@ public ResponseEntity attendanceCheck(@PathVariable var student = userRepository.getUserByLoginElseThrow(studentLogin); - StudentExam studentExam = studentExamRepository.findWithExercisesByUserIdAndExamId(student.getId(), examId, IS_TEST_RUN).orElseThrow(); + StudentExam studentExam = studentExamRepository.findWithExercisesByUserIdAndExamId(student.getId(), examId, false).orElseThrow(); examAccessService.checkCourseAndExamAndStudentExamAccessElseThrow(courseId, examId, studentExam.getId()); @@ -288,8 +286,7 @@ public ResponseEntity attendanceCheck(@PathVariable throw new BadRequestAlertException("Exam is not visible to students", "exam", "examNotVisible"); } - User currentUser = userRepository.getUser(); - var event = examLiveEventsService.createAndSendExamAttendanceCheckEvent(studentExam, message, currentUser); + var event = examLiveEventsService.createAndSendExamAttendanceCheckEvent(studentExam, message); return ResponseEntity.ok(event.asDTO()); } @@ -364,7 +361,7 @@ public ResponseEntity getStudentExamForConduction(@PathVariable Lon HttpServletRequest request) { long start = System.currentTimeMillis(); User currentUser = userRepository.getUserWithGroupsAndAuthorities(); - log.debug("REST request to get the student exam of user {} for exam {}", currentUser.getLogin(), examId); + log.debug("REST request to get the student exam of user {} for exam {} for conduction", currentUser.getLogin(), examId); StudentExam studentExam = studentExamRepository.findByIdWithExercisesElseThrow(studentExamId); diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/repository/TeamRepository.java b/src/main/java/de/tum/cit/aet/artemis/exercise/repository/TeamRepository.java index b6a17585eab3..344ec35c37b2 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/repository/TeamRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/repository/TeamRepository.java @@ -3,7 +3,6 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.LOAD; -import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -139,11 +138,6 @@ default Team save(Exercise exercise, Team team) { if (!conflicts.isEmpty()) { throw new StudentsAlreadyAssignedException(conflicts); } - // audit information is normally updated automatically but since changes in the many-to-many relationships are not registered, - // we need to trigger the audit explicitly by modifying a column of the team entity itself - if (team.getId() != null) { - team.setLastModifiedDate(Instant.now()); - } team.setExercise(exercise); team = save(team); return findWithStudentsByIdElseThrow(team.getId()); diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/service/ExerciseService.java b/src/main/java/de/tum/cit/aet/artemis/exercise/service/ExerciseService.java index ff45bec50524..2d4b4b01c423 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/service/ExerciseService.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/service/ExerciseService.java @@ -796,7 +796,7 @@ public void notifyAboutExerciseChanges(Exercise originalExercise, Exercise updat else if (now().plusMinutes(EXAM_START_WAIT_TIME_MINUTES).isAfter(originalExercise.getExam().getStartDate()) && originalExercise.isExamExercise() && !StringUtils.equals(originalExercise.getProblemStatement(), updatedExercise.getProblemStatement())) { User instructor = userRepository.getUser(); - this.examLiveEventsService.createAndSendProblemStatementUpdateEvent(updatedExercise, notificationText, instructor); + this.examLiveEventsService.createAndSendProblemStatementUpdateEvent(updatedExercise, notificationText); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/plagiarism/web/PlagiarismResultResponseBuilder.java b/src/main/java/de/tum/cit/aet/artemis/plagiarism/web/PlagiarismResultResponseBuilder.java index 3d482e50138e..1bf437cc7353 100644 --- a/src/main/java/de/tum/cit/aet/artemis/plagiarism/web/PlagiarismResultResponseBuilder.java +++ b/src/main/java/de/tum/cit/aet/artemis/plagiarism/web/PlagiarismResultResponseBuilder.java @@ -1,8 +1,5 @@ package de.tum.cit.aet.artemis.plagiarism.web; -import static de.tum.cit.aet.artemis.core.config.Constants.SYSTEM_ACCOUNT; - -import java.util.Objects; import java.util.stream.DoubleStream; import java.util.stream.Stream; @@ -19,8 +16,6 @@ */ public class PlagiarismResultResponseBuilder { - private static final String CONTINUOUS_PLAGIARISM_CONTROL_CREATED_BY_VALUE = "CPC"; - private PlagiarismResultResponseBuilder() { } @@ -41,8 +36,7 @@ public static .flatMap(comparison -> Stream.of(comparison.getSubmissionA().getSubmissionId(), comparison.getSubmissionB().getSubmissionId())).distinct().count(); double averageSimilarity = getSimilarities(plagiarismResult).average().orElse(0.0); double maximalSimilarity = getSimilarities(plagiarismResult).max().orElse(0.0); - var createdBy = getCreatedBy(plagiarismResult); - var stats = new PlagiarismResultStats(numberOfDetectedSubmissions, averageSimilarity, maximalSimilarity, createdBy); + var stats = new PlagiarismResultStats(numberOfDetectedSubmissions, averageSimilarity, maximalSimilarity, plagiarismResult.getCreatedBy()); return ResponseEntity.ok(new PlagiarismResultDTO<>(plagiarismResult, stats)); } @@ -50,13 +44,4 @@ public static private static DoubleStream getSimilarities(PlagiarismResult plagiarismResult) { return plagiarismResult.getComparisons().stream().mapToDouble(PlagiarismComparison::getSimilarity); } - - private static String getCreatedBy(PlagiarismResult result) { - if (Objects.equals(result.getCreatedBy(), SYSTEM_ACCOUNT)) { - return CONTINUOUS_PLAGIARISM_CONTROL_CREATED_BY_VALUE; - } - else { - return result.getCreatedBy(); - } - } } diff --git a/src/test/java/de/tum/cit/aet/artemis/exam/StudentExamIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/exam/StudentExamIntegrationTest.java index 7c54d0eb3528..1327bf765d12 100644 --- a/src/test/java/de/tum/cit/aet/artemis/exam/StudentExamIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/exam/StudentExamIntegrationTest.java @@ -866,7 +866,7 @@ void testExamAnnouncementSent() throws Exception { assertThat(result.id()).isGreaterThan(0L); assertThat(result.text()).isEqualTo(testMessage); - assertThat(result.createdBy()).isEqualTo(userUtilService.getUserByLogin(TEST_PREFIX + "instructor1").getName()); + assertThat(result.createdBy()).isEqualTo(userUtilService.getUserByLogin(TEST_PREFIX + "instructor1").getLogin()); assertThat(result.createdDate()).isCloseTo(Instant.now(), within(5, ChronoUnit.SECONDS)); var event = captureExamLiveEventForId(exam1.getId(), true); @@ -897,7 +897,7 @@ void testExamAttendanceCheck() throws Exception { assertThat(result.id()).isGreaterThan(0L); assertThat(result.text()).isEqualTo(testMessage); - assertThat(result.createdBy()).isEqualTo(userUtilService.getUserByLogin(TEST_PREFIX + "instructor1").getName()); + assertThat(result.createdBy()).isEqualTo(userUtilService.getUserByLogin(TEST_PREFIX + "instructor1").getLogin()); assertThat(result.createdDate()).isCloseTo(Instant.now(), within(5, ChronoUnit.SECONDS)); } diff --git a/src/test/java/de/tum/cit/aet/artemis/exercise/team/TeamFactory.java b/src/test/java/de/tum/cit/aet/artemis/exercise/team/TeamFactory.java index b1fb4f8591a1..d39e091f6561 100644 --- a/src/test/java/de/tum/cit/aet/artemis/exercise/team/TeamFactory.java +++ b/src/test/java/de/tum/cit/aet/artemis/exercise/team/TeamFactory.java @@ -43,10 +43,6 @@ public static Team generateTeamForExercise(Exercise exercise, String name, Strin if (owner != null) { team.setOwner(owner); } - if (creatorLogin != null) { - team.setCreatedBy(creatorLogin); - team.setLastModifiedBy(creatorLogin); - } return team; } diff --git a/src/test/java/de/tum/cit/aet/artemis/exercise/team/TeamImportIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/exercise/team/TeamImportIntegrationTest.java index fc098a2b622f..86516a446971 100644 --- a/src/test/java/de/tum/cit/aet/artemis/exercise/team/TeamImportIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/exercise/team/TeamImportIntegrationTest.java @@ -339,8 +339,9 @@ private void assertCorrectnessOfImport(List expectedTeamsAfterImport, List List destinationTeamsInDatabase = teamRepo.findAllByExerciseId(destinationExercise.getId()); assertThat(actualTeamsAfterImport).as("Imported teams were persisted into destination exercise.").isEqualTo(destinationTeamsInDatabase); - assertThat(actualTeamsAfterImport).as("Teams were correctly imported.").usingRecursiveComparison().ignoringFields("id", "exercise", "createdDate", "lastModifiedDate") - .usingOverriddenEquals().ignoringOverriddenEqualsForTypes(Team.class).ignoringCollectionOrder().isEqualTo(expectedTeamsAfterImport); + assertThat(actualTeamsAfterImport).as("Teams were correctly imported.").usingRecursiveComparison() + .ignoringFields("id", "exercise", "createdDate", "createdBy", "lastModifiedDate", "lastModifiedBy").usingOverriddenEquals() + .ignoringOverriddenEqualsForTypes(Team.class).ignoringCollectionOrder().isEqualTo(expectedTeamsAfterImport); } static List addLists(List a, List b) { 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 d24b66221cf8..c04e4b959c78 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 @@ -59,10 +59,6 @@ public Team generateTeamForExercise(Exercise exercise, String name, String short if (owner != null) { team.setOwner(owner); } - if (creatorLogin != null) { - team.setCreatedBy(creatorLogin); - team.setLastModifiedBy(creatorLogin); - } return team; } diff --git a/src/test/java/de/tum/cit/aet/artemis/fileupload/FileUploadExerciseIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/fileupload/FileUploadExerciseIntegrationTest.java index 99a68bc6f2f1..1cdece99a511 100644 --- a/src/test/java/de/tum/cit/aet/artemis/fileupload/FileUploadExerciseIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/fileupload/FileUploadExerciseIntegrationTest.java @@ -421,7 +421,7 @@ void updateFileUploadExerciseForExam_asInstructor() throws Exception { assertThat(updatedFileUploadExercise.isCourseExercise()).as("course was not set for exam exercise").isFalse(); assertThat(updatedFileUploadExercise.getExerciseGroup()).as("exerciseGroup was set for exam exercise").isNotNull(); assertThat(updatedFileUploadExercise.getExerciseGroup().getId()).as("exerciseGroupId was not updated").isEqualTo(fileUploadExercise.getExerciseGroup().getId()); - verify(examLiveEventsService, timeout(2000).times(1)).createAndSendProblemStatementUpdateEvent(any(), any(), any()); + verify(examLiveEventsService, timeout(2000).times(1)).createAndSendProblemStatementUpdateEvent(any(), any()); verify(groupNotificationScheduleService, never()).checkAndCreateAppropriateNotificationsWhenUpdatingExercise(any(), any(), any()); } diff --git a/src/test/java/de/tum/cit/aet/artemis/modeling/ModelingExerciseIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/modeling/ModelingExerciseIntegrationTest.java index 2891ce7dc87f..2aa8d66fd102 100644 --- a/src/test/java/de/tum/cit/aet/artemis/modeling/ModelingExerciseIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/modeling/ModelingExerciseIntegrationTest.java @@ -2,7 +2,6 @@ import static de.tum.cit.aet.artemis.core.util.TestResourceUtils.HalfSecond; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.timeout; @@ -241,7 +240,7 @@ void testUpdateModelingExercise_asInstructor() throws Exception { params); assertThat(returnedModelingExercise.getGradingCriteria()).hasSameSizeAs(gradingCriteria); verify(groupNotificationService).notifyStudentAndEditorAndInstructorGroupAboutExerciseUpdate(returnedModelingExercise, notificationText); - verify(examLiveEventsService, never()).createAndSendProblemStatementUpdateEvent(eq(returnedModelingExercise), eq(notificationText), any()); + verify(examLiveEventsService, never()).createAndSendProblemStatementUpdateEvent(eq(returnedModelingExercise), eq(notificationText)); verify(competencyProgressApi, timeout(1000).times(1)).updateProgressForUpdatedLearningObjectAsync(eq(createdModelingExercise), eq(Optional.of(createdModelingExercise))); } @@ -268,7 +267,7 @@ void testUpdateModelingExerciseForExam_asInstructor() throws Exception { params); verify(groupNotificationService, never()).notifyStudentAndEditorAndInstructorGroupAboutExerciseUpdate(returnedModelingExercise, notificationText); - verify(examLiveEventsService, times(1)).createAndSendProblemStatementUpdateEvent(eq(returnedModelingExercise), eq(notificationText), any()); + verify(examLiveEventsService, times(1)).createAndSendProblemStatementUpdateEvent(eq(returnedModelingExercise), eq(notificationText)); } @Test diff --git a/src/test/java/de/tum/cit/aet/artemis/plagiarism/PlagiarismResultResponseBuilderTest.java b/src/test/java/de/tum/cit/aet/artemis/plagiarism/PlagiarismResultResponseBuilderTest.java index b15181c3d76a..12a4576adb85 100644 --- a/src/test/java/de/tum/cit/aet/artemis/plagiarism/PlagiarismResultResponseBuilderTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/plagiarism/PlagiarismResultResponseBuilderTest.java @@ -29,7 +29,7 @@ void shouldReturnEmptyResponseForNullResult() { @Test void shouldReturnCorrectResponseForManualChecks() { // given - var plagiarismResult = createPlagiarismResult("user abc"); + var plagiarismResult = createPlagiarismResult(); // when var response = PlagiarismResultResponseBuilder.buildPlagiarismResultResponse(plagiarismResult); @@ -42,13 +42,12 @@ void shouldReturnCorrectResponseForManualChecks() { assertThat(response.getBody().plagiarismResultStats().numberOfDetectedSubmissions()).isEqualTo(3); assertThat(response.getBody().plagiarismResultStats().averageSimilarity()).isEqualTo(0.78); assertThat(response.getBody().plagiarismResultStats().maximalSimilarity()).isEqualTo(0.78); - assertThat(response.getBody().plagiarismResultStats().createdBy()).isEqualTo("user abc"); } @Test void shouldReturnCorrectResponseForCpcChecks() { // given - var plagiarismResult = createPlagiarismResult("system"); + var plagiarismResult = createPlagiarismResult(); // when var response = PlagiarismResultResponseBuilder.buildPlagiarismResultResponse(plagiarismResult); @@ -61,10 +60,9 @@ void shouldReturnCorrectResponseForCpcChecks() { assertThat(response.getBody().plagiarismResultStats().numberOfDetectedSubmissions()).isEqualTo(3); assertThat(response.getBody().plagiarismResultStats().averageSimilarity()).isEqualTo(0.78); assertThat(response.getBody().plagiarismResultStats().maximalSimilarity()).isEqualTo(0.78); - assertThat(response.getBody().plagiarismResultStats().createdBy()).isEqualTo("CPC"); } - private static TextPlagiarismResult createPlagiarismResult(String system) { + private static TextPlagiarismResult createPlagiarismResult() { var submissionA = new PlagiarismSubmission<>(); submissionA.setSubmissionId(1L); var submissionB = new PlagiarismSubmission<>(); @@ -84,7 +82,6 @@ private static TextPlagiarismResult createPlagiarismResult(String system) { var plagiarismResult = new TextPlagiarismResult(); plagiarismResult.setComparisons(Set.of(comparison1, comparison2)); - plagiarismResult.setCreatedBy(system); return plagiarismResult; } diff --git a/src/test/java/de/tum/cit/aet/artemis/text/TextExerciseIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/text/TextExerciseIntegrationTest.java index 06d666cff180..abe893b11770 100644 --- a/src/test/java/de/tum/cit/aet/artemis/text/TextExerciseIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/text/TextExerciseIntegrationTest.java @@ -524,7 +524,7 @@ void updateTextExerciseForExam() throws Exception { assertThat(updatedTextExercise.isCourseExercise()).as("course was not set for exam exercise").isFalse(); assertThat(updatedTextExercise.getExerciseGroup()).as("exerciseGroup was set for exam exercise").isNotNull(); assertThat(updatedTextExercise.getExerciseGroup().getId()).as("exerciseGroupId was not updated").isEqualTo(exerciseGroup.getId()); - verify(examLiveEventsService, timeout(2000).times(1)).createAndSendProblemStatementUpdateEvent(any(), any(), any()); + verify(examLiveEventsService, timeout(2000).times(1)).createAndSendProblemStatementUpdateEvent(any(), any()); verify(groupNotificationScheduleService, never()).checkAndCreateAppropriateNotificationsWhenUpdatingExercise(any(), any(), any()); } @@ -1071,8 +1071,9 @@ void testCheckPlagiarismIdenticalLongTexts() throws Exception { var result = request.get(path, HttpStatus.OK, PlagiarismResultDTO.class, plagiarismUtilService.getDefaultPlagiarismOptions()); assertThat(result.plagiarismResult().getComparisons()).hasSize(1); assertThat(result.plagiarismResult().getExercise().getId()).isEqualTo(textExercise.getId()); + var plagiarismResult = (TextPlagiarismResult) result.plagiarismResult(); - PlagiarismComparison comparison = (PlagiarismComparison) result.plagiarismResult().getComparisons().iterator().next(); + PlagiarismComparison comparison = plagiarismResult.getComparisons().iterator().next(); // Both submissions compared consist of 4 words (= 4 tokens). JPlag seems to be off by 1 // when counting the length of a match. This is why it calculates a similarity of 3/4 = 75% // instead of 4/4 = 100% (5 words ==> 80%, 100 words ==> 99%, etc.). Therefore, we use a rather @@ -1186,7 +1187,6 @@ void testReEvaluateAndUpdateTextExercise() throws Exception { void testReEvaluateAndUpdateTextExerciseWithExampleSubmission() throws Exception { Set gradingCriteria = exerciseUtilService.addGradingInstructionsToExercise(textExercise); gradingCriterionRepository.saveAll(gradingCriteria); - gradingCriteria.remove(1); textExercise.setGradingCriteria(gradingCriteria); // Create example submission @@ -1237,7 +1237,7 @@ void testReEvaluateAndUpdateTextExercise_isNotAtLeastInstructorInCourse_forbidde @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testReEvaluateAndUpdateTextExercise_isNotSameGivenExerciseIdInRequestBody_conflict() throws Exception { - TextExercise textExerciseToBeConflicted = textExerciseRepository.findByCourseIdWithCategories(course.getId()).get(0); + TextExercise textExerciseToBeConflicted = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); textExerciseToBeConflicted.setId(123456789L); textExerciseRepository.save(textExerciseToBeConflicted); From 59066ccaf385c69f8b6409e34fd0c7ed052ed4e1 Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Mon, 24 Feb 2025 09:51:07 +0100 Subject: [PATCH 03/17] Development: Bump version to 8.0.0 (major update) --- README.md | 2 +- build.gradle | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a91c6f7c3b35..b66e37786997 100644 --- a/README.md +++ b/README.md @@ -193,7 +193,7 @@ Refer to [Using JHipster in production](http://www.jhipster.tech/production) for The following command can automate the deployment to a server. The example shows the deployment to the main Artemis test server (which runs a virtual machine): ```shell -./artemis-server-cli deploy username@artemis-test0.artemis.in.tum.de -w build/libs/Artemis-7.10.3.war +./artemis-server-cli deploy username@artemis-test0.artemis.in.tum.de -w build/libs/Artemis-8.0.0.war ``` ## Architecture diff --git a/build.gradle b/build.gradle index c9e2785ec198..68661dd6d076 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,7 @@ plugins { } group = "de.tum.cit.aet.artemis" -version = "7.10.3" +version = "8.0.0" description = "Interactive Learning with Individual Feedback" java { diff --git a/package-lock.json b/package-lock.json index ce911ef41fd1..cf5eba9c202a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "artemis", - "version": "7.10.3", + "version": "8.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "artemis", - "version": "7.10.3", + "version": "8.0.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index a8f011be5570..9f0e98f9851b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "artemis", - "version": "7.10.3", + "version": "8.0.0", "description": "Interactive Learning with Individual Feedback", "private": true, "license": "MIT", From 7f13907745f8e4575ca1925f57cb0eda0043d910 Mon Sep 17 00:00:00 2001 From: Galiiabanu Bakirova <56745022+gbanu@users.noreply.github.com> Date: Tue, 25 Feb 2025 18:33:56 +0100 Subject: [PATCH 04/17] Development: Unify deployment workflow inputs (#10399) --- .github/workflows/staging-deployment.yml | 5 +++++ .github/workflows/testserver-deployment.yml | 3 +++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/staging-deployment.yml b/.github/workflows/staging-deployment.yml index 23e89c909afa..ea4a9bbfa61e 100644 --- a/.github/workflows/staging-deployment.yml +++ b/.github/workflows/staging-deployment.yml @@ -15,6 +15,10 @@ on: type: choice options: - artemis-staging-localci.artemis.cit.tum.de + triggered_by: + description: "Username that triggered deployment (not required, shown if triggered via GitHub UI, logged if triggered via GitHub app)" + required: false + type: string concurrency: ${{ github.event.inputs.environment_name }} @@ -32,6 +36,7 @@ jobs: echo "Branch: ${{ github.event.inputs.branch_name }}" echo "Commit SHA: ${{ github.event.inputs.commit_sha }}" echo "Environment: ${{ github.event.inputs.environment_name }}" + echo "Triggered by: ${{ github.event.inputs.triggered_by }}" - name: Fetch workflow runs by branch and commit id: get_workflow_run diff --git a/.github/workflows/testserver-deployment.yml b/.github/workflows/testserver-deployment.yml index 5a36abd9f66f..92b031af0e99 100644 --- a/.github/workflows/testserver-deployment.yml +++ b/.github/workflows/testserver-deployment.yml @@ -7,6 +7,9 @@ on: description: "Which branch to deploy" required: true type: string + commit_sha: + description: 'Commit SHA to deploy' + required: false environment_name: description: "Which environment to deploy (e.g. artemis-test7.artemis.cit.tum.de, etc.)." required: true From a21ca29bf186dded8e10462feb2618f3d3c67969 Mon Sep 17 00:00:00 2001 From: Simon Entholzer <33342534+SimonEntholzer@users.noreply.github.com> Date: Tue, 25 Feb 2025 18:36:06 +0100 Subject: [PATCH 05/17] Assessment: Fix an edge case issue in course score page with team exercises (#10401) --- .../app/course/course-scores/course-scores.component.ts | 5 ++++- .../team/team-update-dialog/team-update-dialog.component.ts | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/app/course/course-scores/course-scores.component.ts b/src/main/webapp/app/course/course-scores/course-scores.component.ts index e5e79892b647..1cbcf4415672 100644 --- a/src/main/webapp/app/course/course-scores/course-scores.component.ts +++ b/src/main/webapp/app/course/course-scores/course-scores.component.ts @@ -482,7 +482,10 @@ export class CourseScoresComponent implements OnInit, OnDestroy { participation.results?.forEach((result) => (result.participation = participation)); // find all students by iterating through the participations - const participationStudents = participation.student ? [participation.student] : participation.team!.students!; + const participationStudents = participation.student ? [participation.student] : participation.team!.students; + if (!participationStudents) { + continue; + } for (const participationStudent of participationStudents) { let student = studentsMap.get(participationStudent.id!); if (!student) { diff --git a/src/main/webapp/app/exercises/shared/team/team-update-dialog/team-update-dialog.component.ts b/src/main/webapp/app/exercises/shared/team/team-update-dialog/team-update-dialog.component.ts index ec32a5a298d7..0c6c56fcce4f 100644 --- a/src/main/webapp/app/exercises/shared/team/team-update-dialog/team-update-dialog.component.ts +++ b/src/main/webapp/app/exercises/shared/team/team-update-dialog/team-update-dialog.component.ts @@ -123,7 +123,7 @@ export class TeamUpdateDialogComponent implements OnInit { } private get recommendedTeamSize(): boolean { - const pendingTeamSize = this.pendingTeam.students!.length; + const pendingTeamSize = this.pendingTeam.students?.length || 0; return pendingTeamSize >= this.config.minTeamSize! && pendingTeamSize <= this.config.maxTeamSize!; } @@ -162,6 +162,9 @@ export class TeamUpdateDialogComponent implements OnInit { */ onAddStudent(student: User) { if (!this.isStudentAlreadyInPendingTeam(student)) { + if (!this.pendingTeam.students) { + this.pendingTeam.students = []; + } this.pendingTeam.students!.push(student); } } From be00152a8837dd189618f2ae3e47e5ea015172e0 Mon Sep 17 00:00:00 2001 From: Ajayvir Singh <38434017+AjayvirS@users.noreply.github.com> Date: Tue, 25 Feb 2025 19:01:04 +0100 Subject: [PATCH 06/17] Plagiarism checks: Enhance navigation to plagiarism cases from detection page (#10078) --- ...arism-cases-instructor-view.component.html | 2 +- ...giarism-cases-instructor-view.component.ts | 30 +++++++++++++++++-- .../plagiarism-header.component.html | 27 ++++++++++++----- .../plagiarism-header.component.ts | 3 +- src/main/webapp/i18n/de/plagiarism.json | 1 + src/main/webapp/i18n/en/plagiarism.json | 1 + ...sm-cases-instructor-view.component.spec.ts | 21 ++++++++++++- .../plagiarism-header.component.spec.ts | 19 ++++-------- 8 files changed, 77 insertions(+), 27 deletions(-) diff --git a/src/main/webapp/app/course/plagiarism-cases/instructor-view/plagiarism-cases-instructor-view.component.html b/src/main/webapp/app/course/plagiarism-cases/instructor-view/plagiarism-cases-instructor-view.component.html index ba73e8f31b4f..d34a834a6b17 100644 --- a/src/main/webapp/app/course/plagiarism-cases/instructor-view/plagiarism-cases-instructor-view.component.html +++ b/src/main/webapp/app/course/plagiarism-cases/instructor-view/plagiarism-cases-instructor-view.component.html @@ -11,7 +11,7 @@

@for (exercise of exercisesWithPlagiarismCases; track exercise.id; let exerciseIndex = $index) { -
+
diff --git a/src/main/webapp/app/course/plagiarism-cases/instructor-view/plagiarism-cases-instructor-view.component.ts b/src/main/webapp/app/course/plagiarism-cases/instructor-view/plagiarism-cases-instructor-view.component.ts index 3a4137a35740..8d7449b15d1e 100644 --- a/src/main/webapp/app/course/plagiarism-cases/instructor-view/plagiarism-cases-instructor-view.component.ts +++ b/src/main/webapp/app/course/plagiarism-cases/instructor-view/plagiarism-cases-instructor-view.component.ts @@ -1,5 +1,5 @@ import { HttpResponse } from '@angular/common/http'; -import { Component, OnInit, inject } from '@angular/core'; +import { Component, ElementRef, OnInit, effect, inject, viewChildren } from '@angular/core'; import { ActivatedRoute, RouterLink } from '@angular/router'; import { PlagiarismCasesService } from 'app/course/plagiarism-cases/shared/plagiarism-cases.service'; import { PlagiarismCase } from 'app/exercises/shared/plagiarism/types/PlagiarismCase'; @@ -42,6 +42,8 @@ export class PlagiarismCasesInstructorViewComponent implements OnInit { groupedPlagiarismCases: GroupedPlagiarismCases; exercisesWithPlagiarismCases: Exercise[] = []; + exerciseWithPlagCasesElements = viewChildren('plagExerciseElement'); + // method called as html template variable, angular only recognises reference variables in html if they are a property // of the corresponding component class getExerciseUrlSegment = getExerciseUrlSegment; @@ -49,10 +51,20 @@ export class PlagiarismCasesInstructorViewComponent implements OnInit { readonly getIcon = getIcon; readonly documentationType: DocumentationType = 'PlagiarismChecks'; + constructor() { + // effect needs to be in constructor context, due to the possibility of ngOnInit being called from a non-injection + //context + effect(() => { + const exerciseId = Number(this.route.snapshot.queryParamMap?.get('exerciseId')); + if (exerciseId) { + this.scrollToExerciseAfterViewInit(exerciseId); + } + }); + } + ngOnInit(): void { this.courseId = Number(this.route.snapshot.paramMap.get('courseId')); this.examId = Number(this.route.snapshot.paramMap.get('examId')); - const plagiarismCasesForInstructor$ = this.examId ? this.plagiarismCasesService.getExamPlagiarismCasesForInstructor(this.courseId, this.examId) : this.plagiarismCasesService.getCoursePlagiarismCasesForInstructor(this.courseId); @@ -65,6 +77,20 @@ export class PlagiarismCasesInstructorViewComponent implements OnInit { }); } + /** + * scroll to the exercise with + */ + scrollToExerciseAfterViewInit(exerciseId: number) { + const element = this.exerciseWithPlagCasesElements().find((elem) => elem.nativeElement.id === 'exercise-with-plagiarism-case-' + exerciseId); + if (element) { + element.nativeElement.scrollIntoView({ + behavior: 'smooth', + block: 'start', + inline: 'nearest', + }); + } + } + /** * calculate the total number of plagiarism cases * @param plagiarismCases plagiarismCases in the course or exam diff --git a/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-header/plagiarism-header.component.html b/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-header/plagiarism-header.component.html index aff14deb4db6..6976eaeadbc7 100644 --- a/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-header/plagiarism-header.component.html +++ b/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-header/plagiarism-header.component.html @@ -7,20 +7,31 @@
- + @if (comparison.status === plagiarismStatus.CONFIRMED) { + + } @else { + + }
diff --git a/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-header/plagiarism-header.component.ts b/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-header/plagiarism-header.component.ts index 4d496315ddca..c98da380f5b1 100644 --- a/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-header/plagiarism-header.component.ts +++ b/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-header/plagiarism-header.component.ts @@ -10,12 +10,13 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { Exercise, getCourseId } from 'app/entities/exercise.model'; import { TranslateDirective } from 'app/shared/language/translate.directive'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { RouterModule } from '@angular/router'; @Component({ selector: 'jhi-plagiarism-header', styleUrls: ['./plagiarism-header.component.scss'], templateUrl: './plagiarism-header.component.html', - imports: [TranslateDirective, ArtemisTranslatePipe], + imports: [TranslateDirective, ArtemisTranslatePipe, RouterModule], }) export class PlagiarismHeaderComponent { private plagiarismCasesService = inject(PlagiarismCasesService); diff --git a/src/main/webapp/i18n/de/plagiarism.json b/src/main/webapp/i18n/de/plagiarism.json index 31563c61a5cd..e84d01cb1a5d 100644 --- a/src/main/webapp/i18n/de/plagiarism.json +++ b/src/main/webapp/i18n/de/plagiarism.json @@ -3,6 +3,7 @@ "plagiarism": { "plagiarismDetection": "Plagiatskontrolle", "confirm": "Bestätigen", + "viewCases": "Fälle ansehen", "deny": "Ablehnen", "denyAfterConfirmModalTitle": "Wechsel von Bestätigen zu Ablehnen", "denyAfterConfirmModalText": "Bist du dir sicher, dass du die Entscheidung von \"Bestätigung des Plagiats\" in \"Ablehnung\" ändern möchtest? Dadurch wird der entsprechende Plagiatsfall einschließlich der Kommunikation mit dem/der Studierenden und des Urteils gelöscht und kann nicht rückgängig gemacht werden.", diff --git a/src/main/webapp/i18n/en/plagiarism.json b/src/main/webapp/i18n/en/plagiarism.json index db57419b919e..d780b85cd7ea 100644 --- a/src/main/webapp/i18n/en/plagiarism.json +++ b/src/main/webapp/i18n/en/plagiarism.json @@ -3,6 +3,7 @@ "plagiarism": { "plagiarismDetection": "Plagiarism Detection", "confirm": "Confirm", + "viewCases": "View Case(s)", "deny": "Deny", "denyAfterConfirmModalTitle": "Change from confirm to deny", "denyAfterConfirmModalText": "Are you sure that you want to change the decision from confirming the plagiarism to denying it? This will delete the corresponding plagiarism case incl. the communication with the student and the verdict and cannot be undone.", diff --git a/src/test/javascript/spec/component/plagiarism/plagiarism-cases-instructor-view.component.spec.ts b/src/test/javascript/spec/component/plagiarism/plagiarism-cases-instructor-view.component.spec.ts index 3e960f2ae29e..772f98abee76 100644 --- a/src/test/javascript/spec/component/plagiarism/plagiarism-cases-instructor-view.component.spec.ts +++ b/src/test/javascript/spec/component/plagiarism/plagiarism-cases-instructor-view.component.spec.ts @@ -21,7 +21,7 @@ import { ArtemisDatePipe } from '../../../../../main/webapp/app/shared/pipes/art import { ProgressBarComponent } from 'app/shared/dashboards/tutor-participation-graph/progress-bar/progress-bar.component'; import { PlagiarismCaseVerdictComponent } from 'app/course/plagiarism-cases/shared/verdict/plagiarism-case-verdict.component'; import { MockNotificationService } from '../../helpers/mocks/service/mock-notification.service'; -import { Component } from '@angular/core'; +import { Component, ElementRef, signal } from '@angular/core'; import { Location } from '@angular/common'; import { provideHttpClientTesting } from '@angular/common/http/testing'; @@ -251,4 +251,23 @@ describe('Plagiarism Cases Instructor View Component', () => { tick(); expect(location.path()).toBe(`/course-management/${courseId}/${exercise1.type}-exercises/${exerciseId}/plagiarism`); })); + + it('should scroll to the correct exercise element when scrollToExercise is called', () => { + const nativeElement1 = { id: 'exercise-with-plagiarism-case-1', scrollIntoView: jest.fn() }; + const nativeElement2 = { id: 'exercise-with-plagiarism-case-2', scrollIntoView: jest.fn() }; + + const elementRef1 = new ElementRef(nativeElement1); + const elementRef2 = new ElementRef(nativeElement2); + + component.exerciseWithPlagCasesElements = signal([elementRef1, elementRef2]); + + component.scrollToExerciseAfterViewInit(1); + + expect(nativeElement1.scrollIntoView).toHaveBeenCalledWith({ + behavior: 'smooth', + block: 'start', + inline: 'nearest', + }); + expect(nativeElement2.scrollIntoView).not.toHaveBeenCalled(); + }); }); diff --git a/src/test/javascript/spec/component/plagiarism/plagiarism-header.component.spec.ts b/src/test/javascript/spec/component/plagiarism/plagiarism-header.component.spec.ts index 9d3c8c620422..218f1d070478 100644 --- a/src/test/javascript/spec/component/plagiarism/plagiarism-header.component.spec.ts +++ b/src/test/javascript/spec/component/plagiarism/plagiarism-header.component.spec.ts @@ -1,6 +1,6 @@ -import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { TranslateService } from '@ngx-translate/core'; -import { Observable, Subject, of } from 'rxjs'; +import { Observable, of, Subject } from 'rxjs'; import { PlagiarismHeaderComponent } from 'app/exercises/shared/plagiarism/plagiarism-header/plagiarism-header.component'; import { MockTranslateService } from '../../helpers/mocks/service/mock-translate.service'; import { PlagiarismComparison } from 'app/exercises/shared/plagiarism/types/PlagiarismComparison'; @@ -57,17 +57,6 @@ describe('Plagiarism Header Component', () => { expect(comp.isLoading).toBeTrue(); }); - it('should disable confirm button if plagiarism status is dirty', () => { - comp.comparison.status = PlagiarismStatus.NONE; - comp.isLoading = true; - - const nativeElement = fixture.nativeElement; - const button = nativeElement.querySelector("[data-qa='confirm-plagiarism-button']") as ButtonComponent; - fixture.detectChanges(); - - expect(button.disabled).toBeTrue(); - }); - it('should deny a plagiarism', () => { jest.spyOn(comp, 'updatePlagiarismStatus'); comp.denyPlagiarism(); @@ -77,7 +66,7 @@ describe('Plagiarism Header Component', () => { }); it('should disable deny button if plagiarism status is dirty', () => { - comp.comparison.status = PlagiarismStatus.NONE; + comp.comparison.status = PlagiarismStatus.DENIED; comp.isLoading = true; const nativeElement = fixture.nativeElement; @@ -154,6 +143,8 @@ describe('Plagiarism Header Component', () => { it.each(['confirm-plagiarism-button', 'deny-plagiarism-button'])('should disable status update button for team exercises', (selector) => { comp.exercise.teamMode = true; + comp.comparison.status = PlagiarismStatus.NONE; + fixture.detectChanges(); const nativeElement = fixture.nativeElement; const button = nativeElement.querySelector(`[data-qa=${selector}]`) as ButtonComponent; From 7b826ee5d83a4e031f7f9b7a4079855ec56fef88 Mon Sep 17 00:00:00 2001 From: Simon Entholzer <33342534+SimonEntholzer@users.noreply.github.com> Date: Tue, 25 Feb 2025 19:02:31 +0100 Subject: [PATCH 07/17] Programming exercises: Add notifications for SSH and personal VCS access token creation and expiry (#10358) --- .../domain/NotificationType.java | 3 +- .../notification/NotificationConstants.java | 35 ++- .../SingleUserNotificationFactory.java | 63 +++++ .../service/notifications/MailService.java | 27 ++- .../NotificationSettingsService.java | 39 ++- .../SingleUserNotificationService.java | 67 ++++++ .../core/repository/UserRepository.java | 4 + .../core/service/user/UserService.java | 9 +- .../aet/artemis/core/web/AccountResource.java | 9 +- .../UserSshPublicKeyRepository.java | 9 + ...SshPublicKeyExpiryNotificationService.java | 77 ++++++ .../UserSshPublicKeyService.java | 25 +- .../UserTokenExpiryNotificationService.java | 69 ++++++ .../SshPublicKeysResource.java | 5 +- .../changelog/20241213144500_changelog.xml | 12 + .../resources/config/liquibase/master.xml | 3 +- src/main/resources/i18n/messages.properties | 34 ++- .../resources/i18n/messages_de.properties | 28 ++- .../resources/i18n/messages_en.properties | 25 ++ .../mail/notification/sshKeyAddedEmail.html | 42 ++++ .../notification/sshKeyExpiresSoonEmail.html | 40 ++++ .../notification/sshKeyHasExpiredEmail.html | 40 ++++ .../vcsAccessTokenAddedEmail.html | 34 +++ .../vcsAccessTokenExpiredEmail.html | 28 +++ .../vcsAccessTokenExpiresSoonEmail.html | 28 +++ ...h-user-settings-key-details.component.html | 3 +- ...ssh-user-settings-key-details.component.ts | 5 + src/main/webapp/i18n/de/notification.json | 16 +- src/main/webapp/i18n/en/notification.json | 16 +- .../GroupNotificationServiceTest.java | 16 +- .../NotificationResourceIntegrationTest.java | 60 ++--- .../NotificationScheduleServiceTest.java | 10 +- .../SingleUserNotificationServiceTest.java | 225 ++++++++++++++++-- .../ConversationNotificationServiceTest.java | 6 +- .../NotificationTestRepository.java | 23 ++ .../core/user/util/UserTestService.java | 7 + .../icl/LocalVCSshSettingsTest.java | 6 + .../icl/util/SshSettingsTestService.java | 5 - 38 files changed, 1058 insertions(+), 95 deletions(-) create mode 100644 src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyExpiryNotificationService.java rename src/main/java/de/tum/cit/aet/artemis/programming/service/{ => sshuserkeys}/UserSshPublicKeyService.java (86%) create mode 100644 src/main/java/de/tum/cit/aet/artemis/programming/service/tokens/UserTokenExpiryNotificationService.java rename src/main/java/de/tum/cit/aet/artemis/programming/web/{localvc/ssh => sshuserkeys}/SshPublicKeysResource.java (97%) create mode 100644 src/main/resources/config/liquibase/changelog/20241213144500_changelog.xml create mode 100644 src/main/resources/templates/mail/notification/sshKeyAddedEmail.html create mode 100644 src/main/resources/templates/mail/notification/sshKeyExpiresSoonEmail.html create mode 100644 src/main/resources/templates/mail/notification/sshKeyHasExpiredEmail.html create mode 100644 src/main/resources/templates/mail/notification/vcsAccessTokenAddedEmail.html create mode 100644 src/main/resources/templates/mail/notification/vcsAccessTokenExpiredEmail.html create mode 100644 src/main/resources/templates/mail/notification/vcsAccessTokenExpiresSoonEmail.html create mode 100644 src/test/java/de/tum/cit/aet/artemis/core/test_repository/NotificationTestRepository.java diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/domain/NotificationType.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/NotificationType.java index d086f05e7ee9..5e620b2eae79 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/domain/NotificationType.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/NotificationType.java @@ -9,5 +9,6 @@ public enum NotificationType { TUTORIAL_GROUP_MULTIPLE_REGISTRATION_TUTOR, TUTORIAL_GROUP_DEREGISTRATION_TUTOR, TUTORIAL_GROUP_DELETED, TUTORIAL_GROUP_UPDATED, TUTORIAL_GROUP_ASSIGNED, TUTORIAL_GROUP_UNASSIGNED, CONVERSATION_NEW_MESSAGE, CONVERSATION_NEW_REPLY_MESSAGE, CONVERSATION_USER_MENTIONED, CONVERSATION_CREATE_ONE_TO_ONE_CHAT, CONVERSATION_CREATE_GROUP_CHAT, CONVERSATION_ADD_USER_GROUP_CHAT, CONVERSATION_ADD_USER_CHANNEL, CONVERSATION_REMOVE_USER_GROUP_CHAT, CONVERSATION_REMOVE_USER_CHANNEL, - CONVERSATION_DELETE_CHANNEL, DATA_EXPORT_CREATED, DATA_EXPORT_FAILED, PROGRAMMING_REPOSITORY_LOCKS, PROGRAMMING_BUILD_RUN_UPDATE + CONVERSATION_DELETE_CHANNEL, DATA_EXPORT_CREATED, DATA_EXPORT_FAILED, PROGRAMMING_REPOSITORY_LOCKS, PROGRAMMING_BUILD_RUN_UPDATE, SSH_KEY_ADDED, SSH_KEY_EXPIRES_SOON, + SSH_KEY_HAS_EXPIRED, VCS_ACCESS_TOKEN_ADDED, VCS_ACCESS_TOKEN_EXPIRED, VCS_ACCESS_TOKEN_EXPIRES_SOON } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/NotificationConstants.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/NotificationConstants.java index 5456e6c15636..f3a86cbd1a0e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/NotificationConstants.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/NotificationConstants.java @@ -44,6 +44,9 @@ import static de.tum.cit.aet.artemis.communication.domain.NotificationType.PROGRAMMING_REPOSITORY_LOCKS; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.PROGRAMMING_TEST_CASES_CHANGED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.QUIZ_EXERCISE_STARTED; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_ADDED; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_EXPIRES_SOON; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_HAS_EXPIRED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_ASSIGNED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_DELETED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_DEREGISTRATION_STUDENT; @@ -53,6 +56,9 @@ import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_REGISTRATION_TUTOR; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_UNASSIGNED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_UPDATED; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.VCS_ACCESS_TOKEN_ADDED; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.VCS_ACCESS_TOKEN_EXPIRED; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.VCS_ACCESS_TOKEN_EXPIRES_SOON; import com.google.common.collect.BiMap; import com.google.common.collect.ImmutableBiMap; @@ -150,6 +156,18 @@ public class NotificationConstants { public static final String TUTORIAL_GROUP_UNASSIGNED_TITLE = "artemisApp.singleUserNotification.title.tutorialGroupUnassigned"; + public static final String SSH_KEY_ADDED_TITLE = "artemisApp.singleUserNotification.title.sshKeyAdded"; + + public static final String SSH_KEY_EXPIRES_SOON_TITLE = "artemisApp.singleUserNotification.title.sshKeyExpiresSoon"; + + public static final String SSH_KEY_HAS_EXPIRED_TITLE = "artemisApp.singleUserNotification.title.sshKeyHasExpired"; + + public static final String VCS_ACCESS_TOKEN_ADDED_TITLE = "artemisApp.singleUserNotification.title.vcsAccessTokenAdded"; + + public static final String VCS_ACCESS_TOKEN_EXPIRED_TITLE = "artemisApp.singleUserNotification.title.vcsAccessTokenExpired"; + + public static final String VCS_ACCESS_TOKEN_EXPIRES_SOON_TITLE = "artemisApp.singleUserNotification.title.vcsAccessTokenExpiresSoon"; + // Texts public static final String LIVE_EXAM_EXERCISE_UPDATE_NOTIFICATION_TEXT = "artemisApp.groupNotification.text.liveExamExerciseUpdate"; @@ -285,6 +303,18 @@ public class NotificationConstants { public static final String CONVERSATION_DELETE_CHANNEL_TEXT = "artemisApp.singleUserNotification.text.deleteChannel"; + public static final String SSH_KEY_ADDED_TEXT = "artemisApp.singleUserNotification.text.sshKeyAdded"; + + public static final String SSH_KEY_EXPIRES_SOON_TEXT = "artemisApp.singleUserNotification.text.sshKeyExpiresSoon"; + + public static final String SSH_KEY_HAS_EXPIRED_TEXT = "artemisApp.singleUserNotification.text.sshKeyHasExpired"; + + public static final String VCS_ACCESS_TOKEN_ADDED_TEXT = "artemisApp.singleUserNotification.text.vcsAccessTokenAdded"; + + public static final String VCS_ACCESS_TOKEN_EXPIRED_TEXT = "artemisApp.singleUserNotification.text.vcsAccessTokenExpired"; + + public static final String VCS_ACCESS_TOKEN_EXPIRES_SOON_TEXT = "artemisApp.singleUserNotification.text.vcsAccessTokenExpiresSoon"; + // bidirectional map private static final BiMap NOTIFICATION_TYPE_AND_TITLE_MAP = new ImmutableBiMap.Builder() .put(EXERCISE_SUBMISSION_ASSESSED, EXERCISE_SUBMISSION_ASSESSED_TITLE).put(ATTACHMENT_CHANGE, ATTACHMENT_CHANGE_TITLE).put(EXERCISE_RELEASED, EXERCISE_RELEASED_TITLE) @@ -310,7 +340,10 @@ public class NotificationConstants { .put(CONVERSATION_ADD_USER_GROUP_CHAT, CONVERSATION_ADD_USER_GROUP_CHAT_TITLE).put(CONVERSATION_REMOVE_USER_GROUP_CHAT, CONVERSATION_REMOVE_USER_GROUP_CHAT_TITLE) .put(CONVERSATION_REMOVE_USER_CHANNEL, CONVERSATION_REMOVE_USER_CHANNEL_TITLE).put(CONVERSATION_DELETE_CHANNEL, CONVERSATION_DELETE_CHANNEL_TITLE) .put(DATA_EXPORT_CREATED, DATA_EXPORT_CREATED_TITLE).put(DATA_EXPORT_FAILED, DATA_EXPORT_FAILED_TITLE) - .put(PROGRAMMING_REPOSITORY_LOCKS, PROGRAMMING_REPOSITORY_LOCKS_TITLE).put(PROGRAMMING_BUILD_RUN_UPDATE, PROGRAMMING_BUILD_RUN_UPDATE_TITLE).build(); + .put(PROGRAMMING_REPOSITORY_LOCKS, PROGRAMMING_REPOSITORY_LOCKS_TITLE).put(PROGRAMMING_BUILD_RUN_UPDATE, PROGRAMMING_BUILD_RUN_UPDATE_TITLE) + .put(SSH_KEY_ADDED, SSH_KEY_ADDED_TITLE).put(SSH_KEY_EXPIRES_SOON, SSH_KEY_EXPIRES_SOON_TITLE).put(SSH_KEY_HAS_EXPIRED, SSH_KEY_HAS_EXPIRED_TITLE) + .put(VCS_ACCESS_TOKEN_ADDED, VCS_ACCESS_TOKEN_ADDED_TITLE).put(VCS_ACCESS_TOKEN_EXPIRED, VCS_ACCESS_TOKEN_EXPIRED_TITLE) + .put(VCS_ACCESS_TOKEN_EXPIRES_SOON, VCS_ACCESS_TOKEN_EXPIRES_SOON_TITLE).build(); /** * Finds the corresponding NotificationType for the provided notification title diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java index 6e812844841a..3d24a7cee7d7 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java @@ -21,6 +21,8 @@ import static de.tum.cit.aet.artemis.communication.domain.NotificationType.NEW_REPLY_FOR_EXERCISE_POST; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.NEW_REPLY_FOR_LECTURE_POST; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.PLAGIARISM_CASE_VERDICT_STUDENT; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_EXPIRES_SOON; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_HAS_EXPIRED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_ASSIGNED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_DEREGISTRATION_STUDENT; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_DEREGISTRATION_TUTOR; @@ -51,6 +53,12 @@ import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.PLAGIARISM_CASE_REPLY_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.PLAGIARISM_CASE_VERDICT_STUDENT_TEXT; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.PLAGIARISM_CASE_VERDICT_STUDENT_TITLE; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.SSH_KEY_ADDED_TEXT; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.SSH_KEY_ADDED_TITLE; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.SSH_KEY_EXPIRES_SOON_TEXT; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.SSH_KEY_EXPIRES_SOON_TITLE; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.SSH_KEY_HAS_EXPIRED_TEXT; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.SSH_KEY_HAS_EXPIRED_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_ASSIGNED_TEXT; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_DEREGISTRATION_STUDENT_TEXT; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_DEREGISTRATION_TUTOR_TEXT; @@ -58,6 +66,12 @@ import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_REGISTRATION_STUDENT_TEXT; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_REGISTRATION_TUTOR_TEXT; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_UNASSIGNED_TEXT; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.VCS_ACCESS_TOKEN_ADDED_TEXT; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.VCS_ACCESS_TOKEN_ADDED_TITLE; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.VCS_ACCESS_TOKEN_EXPIRED_TEXT; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.VCS_ACCESS_TOKEN_EXPIRED_TITLE; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.VCS_ACCESS_TOKEN_EXPIRES_SOON_TEXT; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.VCS_ACCESS_TOKEN_EXPIRES_SOON_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.findCorrespondingNotificationTitleOrThrow; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationTargetFactory.createConversationCreationTarget; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationTargetFactory.createConversationDeletionTarget; @@ -82,6 +96,7 @@ import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.exercise.domain.Exercise; import de.tum.cit.aet.artemis.plagiarism.domain.PlagiarismCase; +import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey; import de.tum.cit.aet.artemis.tutorialgroup.domain.TutorialGroup; public class SingleUserNotificationFactory { @@ -160,6 +175,54 @@ public static String[] createPlaceholdersDataExport() { return new String[] {}; } + /** + * Creates a user notification based on the given SSH key and notification type. + * + * @param key The SSH key related to the notification (currently unused). + * @param notificationType The type of notification to create (e.g., key added, expiring, or expired). + * @param recipient The user who will receive the notification. + * @return A configured {@link SingleUserNotification}. + * @throws UnsupportedOperationException if the notification type is unsupported. + */ + public static SingleUserNotification createNotification(UserSshPublicKey key, NotificationType notificationType, User recipient) { + switch (notificationType) { + case SSH_KEY_ADDED -> { + return new SingleUserNotification(recipient, SSH_KEY_ADDED_TITLE, SSH_KEY_ADDED_TEXT, true, new String[] {}); + } + case SSH_KEY_EXPIRES_SOON -> { + return new SingleUserNotification(recipient, SSH_KEY_EXPIRES_SOON_TITLE, SSH_KEY_EXPIRES_SOON_TEXT, true, new String[] {}); + } + case SSH_KEY_HAS_EXPIRED -> { + return new SingleUserNotification(recipient, SSH_KEY_HAS_EXPIRED_TITLE, SSH_KEY_HAS_EXPIRED_TEXT, true, new String[] {}); + } + default -> throw new UnsupportedOperationException("Unsupported NotificationType: " + notificationType); + } + } + + /** + * Creates a user notification based on the given SSH key and notification type. + * + * @param vcsAccessToken The access token of the user + * @param notificationType The type of notification to create (e.g., key added, expiring, or expired). + * @param recipient The user who will receive the notification. + * @return A configured {@link SingleUserNotification}. + * @throws UnsupportedOperationException if the notification type is unsupported. + */ + public static SingleUserNotification createNotification(String vcsAccessToken, NotificationType notificationType, User recipient) { + switch (notificationType) { + case VCS_ACCESS_TOKEN_ADDED -> { + return new SingleUserNotification(recipient, VCS_ACCESS_TOKEN_ADDED_TITLE, VCS_ACCESS_TOKEN_ADDED_TEXT, true, new String[] {}); + } + case VCS_ACCESS_TOKEN_EXPIRED -> { + return new SingleUserNotification(recipient, VCS_ACCESS_TOKEN_EXPIRED_TITLE, VCS_ACCESS_TOKEN_EXPIRED_TEXT, true, new String[] {}); + } + case VCS_ACCESS_TOKEN_EXPIRES_SOON -> { + return new SingleUserNotification(recipient, VCS_ACCESS_TOKEN_EXPIRES_SOON_TITLE, VCS_ACCESS_TOKEN_EXPIRES_SOON_TEXT, true, new String[] {}); + } + default -> throw new UnsupportedOperationException("Unsupported NotificationType: " + notificationType); + } + } + /** * Creates an instance of SingleUserNotification based on plagiarisms. * diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/MailService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/MailService.java index 4a17602cc53d..a5a1ef622193 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/MailService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/MailService.java @@ -5,6 +5,7 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import java.net.URL; +import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -33,6 +34,7 @@ import de.tum.cit.aet.artemis.exercise.domain.Exercise; import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation; import de.tum.cit.aet.artemis.plagiarism.domain.PlagiarismCase; +import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey; /** * Service for preparing and sending emails. @@ -55,9 +57,14 @@ public class MailService implements InstantNotificationService { private static final String REASON = "reason"; + private static final String CONTACT_EMAIL = "contactEmail"; + @Value("${server.url}") private URL artemisServerUrl; + @Value("${info.contact}") + private String contactEmailAddress; + private final MessageSource messageSource; private final SpringTemplateEngine templateEngine; @@ -86,6 +93,10 @@ public class MailService implements InstantNotificationService { private static final String NOTIFICATION_TYPE = "notificationType"; + private static final String SSH_KEY = "sshKey"; + + private static final String SSH_KEY_EXPIRY_DATE = "expiryDate"; + // time related variables private static final String TIME_SERVICE = "timeService"; @@ -252,7 +263,7 @@ public void sendNotification(Notification notification, User user, Object notifi context.setVariable(USER, user); context.setVariable(NOTIFICATION, notification); context.setVariable(NOTIFICATION_SUBJECT, notificationSubject); - + context.setVariable(CONTACT_EMAIL, contactEmailAddress); context.setVariable(TIME_SERVICE, this.timeService); String subject = messageSource.getMessage(notification.getTitle(), null, context.getLocale()); @@ -263,7 +274,12 @@ public void sendNotification(Notification notification, User user, Object notifi if (notificationSubject instanceof PlagiarismCase plagiarismCase) { subject = setPlagiarismContextAndSubject(context, notificationType, notification, plagiarismCase); } - + if (notificationSubject instanceof UserSshPublicKey userSshPublicKey) { + context.setVariable(SSH_KEY, userSshPublicKey); + if (userSshPublicKey.getExpiryDate() != null) { + context.setVariable(SSH_KEY_EXPIRY_DATE, userSshPublicKey.getExpiryDate().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"))); + } + } if (notificationSubject instanceof SingleUserNotificationService.TutorialGroupNotificationSubject tutorialGroupNotificationSubject) { setContextForTutorialGroupNotifications(context, notificationType, tutorialGroupNotificationSubject); } @@ -401,6 +417,13 @@ private String createContentForNotificationEmailByType(NotificationType notifica case TUTORIAL_GROUP_UPDATED -> templateEngine.process("mail/notification/tutorialGroupUpdatedEmail", context); case DATA_EXPORT_CREATED -> templateEngine.process("mail/notification/dataExportCreatedEmail", context); case DATA_EXPORT_FAILED -> templateEngine.process("mail/notification/dataExportFailedEmail", context); + case SSH_KEY_ADDED -> templateEngine.process("mail/notification/sshKeyAddedEmail", context); + case SSH_KEY_EXPIRES_SOON -> templateEngine.process("mail/notification/sshKeyExpiresSoonEmail", context); + case SSH_KEY_HAS_EXPIRED -> templateEngine.process("mail/notification/sshKeyHasExpiredEmail", context); + case VCS_ACCESS_TOKEN_ADDED -> templateEngine.process("mail/notification/vcsAccessTokenAddedEmail", context); + case VCS_ACCESS_TOKEN_EXPIRED -> templateEngine.process("mail/notification/vcsAccessTokenExpiredEmail", context); + case VCS_ACCESS_TOKEN_EXPIRES_SOON -> templateEngine.process("mail/notification/vcsAccessTokenExpiresSoonEmail", context); + default -> throw new UnsupportedOperationException("Unsupported NotificationType: " + notificationType); }; } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/NotificationSettingsService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/NotificationSettingsService.java index f5ea4060cea1..bf9c969e48b3 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/NotificationSettingsService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/NotificationSettingsService.java @@ -33,6 +33,9 @@ import static de.tum.cit.aet.artemis.communication.domain.NotificationType.PLAGIARISM_CASE_VERDICT_STUDENT; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.PROGRAMMING_TEST_CASES_CHANGED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.QUIZ_EXERCISE_STARTED; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_ADDED; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_EXPIRES_SOON; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_HAS_EXPIRED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_ASSIGNED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_DELETED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_DEREGISTRATION_STUDENT; @@ -42,6 +45,9 @@ import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_REGISTRATION_TUTOR; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_UNASSIGNED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_UPDATED; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.VCS_ACCESS_TOKEN_ADDED; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.VCS_ACCESS_TOKEN_EXPIRED; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.VCS_ACCESS_TOKEN_EXPIRES_SOON; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.findCorrespondingNotificationType; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; @@ -136,6 +142,19 @@ public class NotificationSettingsService { public static final String NOTIFICATION_USER_NOTIFICATION_DATA_EXPORT_FAILED = "notification.user-notification.data-export-failed"; + // ssh user notification settings group + public static final String NOTIFICATION_USER_NOTIFICATION_SSH_KEY_ADDED = "notification.user-notification.ssh-key-added"; + + public static final String NOTIFICATION_USER_NOTIFICATION_SSH_KEY_EXPIRES_SOON = "notification.user-notification.ssh-key-expires-soon"; + + public static final String NOTIFICATION_USER_NOTIFICATION_SSH_KEY_HAS_EXPIRED = "notification.user-notification.ssh-key-has-expired"; + + public static final String NOTIFICATION_USER_NOTIFICATION_VCS_ACCESS_TOKEN_ADDED = "notification.user-notification.vcs-access-token-added"; + + public static final String NOTIFICATION_USER_NOTIFICATION_VCS_ACCESS_TOKEN_EXPIRED = "notification.user-notification.vcs-access-token-expired"; + + public static final String NOTIFICATION_USER_NOTIFICATION_VCS_ACCESS_TOKEN_EXPIRES_SOON = "notification.user-notification.vcs-access-token-expires-soon"; + // if webapp or email is not explicitly set for a specific setting -> no support for this communication channel for this setting // this has to match the properties in the notification settings structure file on the client that hides the related UI elements public static final Set DEFAULT_NOTIFICATION_SETTINGS = new HashSet<>(Arrays.asList( @@ -175,9 +194,15 @@ public class NotificationSettingsService { new NotificationSetting(true, false, true, NOTIFICATION__USER_NOTIFICATION__NEW_REPLY_IN_CONVERSATION_MESSAGE), // user mention notification setting group new NotificationSetting(true, false, true, NOTIFICATION__USER_NOTIFICATION__USER_MENTION), - // data export notification setting (cannot be overridden by user) + // data export and SSH notification setting (cannot be overridden by user) new NotificationSetting(true, true, true, NOTIFICATION_USER_NOTIFICATION_DATA_EXPORT_FAILED), - new NotificationSetting(true, true, true, NOTIFICATION_USER_NOTIFICATION_DATA_EXPORT_CREATED))); + new NotificationSetting(true, true, true, NOTIFICATION_USER_NOTIFICATION_DATA_EXPORT_CREATED), + new NotificationSetting(true, true, false, NOTIFICATION_USER_NOTIFICATION_SSH_KEY_ADDED), + new NotificationSetting(true, true, false, NOTIFICATION_USER_NOTIFICATION_SSH_KEY_EXPIRES_SOON), + new NotificationSetting(true, true, false, NOTIFICATION_USER_NOTIFICATION_SSH_KEY_HAS_EXPIRED), + new NotificationSetting(true, true, false, NOTIFICATION_USER_NOTIFICATION_VCS_ACCESS_TOKEN_ADDED), + new NotificationSetting(true, true, false, NOTIFICATION_USER_NOTIFICATION_VCS_ACCESS_TOKEN_EXPIRED), + new NotificationSetting(true, true, false, NOTIFICATION_USER_NOTIFICATION_VCS_ACCESS_TOKEN_EXPIRES_SOON))); /** * This is the place where the mapping between SettingId and NotificationTypes happens on the server side @@ -222,6 +247,13 @@ public class NotificationSettingsService { map.put(NOTIFICATION__USER_NOTIFICATION__NEW_REPLY_IN_CONVERSATION_MESSAGE, new NotificationType[]{CONVERSATION_NEW_REPLY_MESSAGE}); map.put(NOTIFICATION__USER_NOTIFICATION__USER_MENTION, new NotificationType[]{CONVERSATION_USER_MENTIONED}); + map.put(NOTIFICATION_USER_NOTIFICATION_SSH_KEY_ADDED, new NotificationType[] { SSH_KEY_ADDED }); + map.put(NOTIFICATION_USER_NOTIFICATION_SSH_KEY_EXPIRES_SOON, new NotificationType[] { SSH_KEY_EXPIRES_SOON }); + map.put(NOTIFICATION_USER_NOTIFICATION_SSH_KEY_HAS_EXPIRED, new NotificationType[] { SSH_KEY_HAS_EXPIRED }); + map.put(NOTIFICATION_USER_NOTIFICATION_VCS_ACCESS_TOKEN_ADDED, new NotificationType[] { VCS_ACCESS_TOKEN_ADDED }); + map.put(NOTIFICATION_USER_NOTIFICATION_VCS_ACCESS_TOKEN_EXPIRED, new NotificationType[] { VCS_ACCESS_TOKEN_EXPIRED }); + map.put(NOTIFICATION_USER_NOTIFICATION_VCS_ACCESS_TOKEN_EXPIRES_SOON, new NotificationType[] { VCS_ACCESS_TOKEN_EXPIRES_SOON }); + NOTIFICATION_SETTING_ID_TO_NOTIFICATION_TYPES_MAP = Collections.unmodifiableMap(map); } // @formatter:on @@ -234,7 +266,8 @@ public class NotificationSettingsService { PLAGIARISM_CASE_VERDICT_STUDENT, PLAGIARISM_CASE_REPLY, TUTORIAL_GROUP_REGISTRATION_STUDENT, TUTORIAL_GROUP_REGISTRATION_TUTOR, TUTORIAL_GROUP_MULTIPLE_REGISTRATION_TUTOR, TUTORIAL_GROUP_DEREGISTRATION_STUDENT, TUTORIAL_GROUP_DEREGISTRATION_TUTOR, TUTORIAL_GROUP_DELETED, TUTORIAL_GROUP_UPDATED, TUTORIAL_GROUP_ASSIGNED, TUTORIAL_GROUP_UNASSIGNED, NEW_EXERCISE_POST, NEW_LECTURE_POST, NEW_REPLY_FOR_LECTURE_POST, NEW_COURSE_POST, NEW_REPLY_FOR_COURSE_POST, - NEW_REPLY_FOR_EXERCISE_POST, QUIZ_EXERCISE_STARTED, DATA_EXPORT_CREATED, DATA_EXPORT_FAILED, CONVERSATION_NEW_MESSAGE, CONVERSATION_NEW_REPLY_MESSAGE); + NEW_REPLY_FOR_EXERCISE_POST, QUIZ_EXERCISE_STARTED, DATA_EXPORT_CREATED, DATA_EXPORT_FAILED, CONVERSATION_NEW_MESSAGE, CONVERSATION_NEW_REPLY_MESSAGE, SSH_KEY_ADDED, + SSH_KEY_EXPIRES_SOON, SSH_KEY_HAS_EXPIRED, VCS_ACCESS_TOKEN_ADDED, VCS_ACCESS_TOKEN_EXPIRED, VCS_ACCESS_TOKEN_EXPIRES_SOON); // More information on supported notification types can be found here: https://docs.artemis.cit.tum.de/user/notifications/ // Please adapt the above docs if you change the supported notification types diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/SingleUserNotificationService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/SingleUserNotificationService.java index 3ceb622c8458..9b25ecdf801c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/SingleUserNotificationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/SingleUserNotificationService.java @@ -14,6 +14,9 @@ import static de.tum.cit.aet.artemis.communication.domain.NotificationType.NEW_REPLY_FOR_LECTURE_POST; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.PLAGIARISM_CASE_REPLY; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.PLAGIARISM_CASE_VERDICT_STUDENT; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_ADDED; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_EXPIRES_SOON; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_HAS_EXPIRED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_ASSIGNED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_DEREGISTRATION_STUDENT; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_DEREGISTRATION_TUTOR; @@ -21,6 +24,9 @@ import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_REGISTRATION_STUDENT; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_REGISTRATION_TUTOR; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_UNASSIGNED; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.VCS_ACCESS_TOKEN_ADDED; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.VCS_ACCESS_TOKEN_EXPIRED; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.VCS_ACCESS_TOKEN_EXPIRES_SOON; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.CONVERSATION_ADD_USER_CHANNEL_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.CONVERSATION_ADD_USER_GROUP_CHAT_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.CONVERSATION_CREATE_GROUP_CHAT_TITLE; @@ -72,6 +78,7 @@ import de.tum.cit.aet.artemis.exercise.service.ExerciseDateService; import de.tum.cit.aet.artemis.fileupload.domain.FileUploadExercise; import de.tum.cit.aet.artemis.plagiarism.domain.PlagiarismCase; +import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey; import de.tum.cit.aet.artemis.tutorialgroup.domain.TutorialGroup; @Profile(PROFILE_CORE) @@ -146,6 +153,9 @@ private SingleUserNotification createSingleUserNotification(Object notificationS createNotification(((NewReplyNotificationSubject) notificationSubject).answerPost, notificationType, ((NewReplyNotificationSubject) notificationSubject).user, ((NewReplyNotificationSubject) notificationSubject).responsibleUser); case DATA_EXPORT_CREATED, DATA_EXPORT_FAILED -> createNotification((DataExport) notificationSubject, notificationType, typeSpecificInformation); + case SSH_KEY_ADDED, SSH_KEY_EXPIRES_SOON, SSH_KEY_HAS_EXPIRED -> createNotification((UserSshPublicKey) notificationSubject, notificationType, typeSpecificInformation); + case VCS_ACCESS_TOKEN_ADDED, VCS_ACCESS_TOKEN_EXPIRED, VCS_ACCESS_TOKEN_EXPIRES_SOON -> + createNotification(typeSpecificInformation.getVcsAccessToken(), notificationType, typeSpecificInformation); default -> throw new UnsupportedOperationException("Can not create notification for type : " + notificationType); }; } @@ -257,6 +267,63 @@ public void notifyUserAboutDataExportFailure(DataExport dataExport) { notifyRecipientWithNotificationType(dataExport, DATA_EXPORT_FAILED, dataExport.getUser(), null); } + /** + * Notify user about the addition of an SSH key in the settings + * + * @param recipient the user to whose account the SSH key was added + * @param key the key which was added + */ + public void notifyUserAboutNewlyAddedSshKey(User recipient, UserSshPublicKey key) { + notifyRecipientWithNotificationType(key, SSH_KEY_ADDED, recipient, null); + } + + /** + * Notify user about an upcoming expiry of an SSH key + * + * @param recipient the user of whose account the SSH key will expire soon + * @param key the key which was added + */ + public void notifyUserAboutSoonExpiringSshKey(User recipient, UserSshPublicKey key) { + notifyRecipientWithNotificationType(key, SSH_KEY_EXPIRES_SOON, recipient, null); + } + + /** + * Notify user about the expiration of an SSH key + * + * @param recipient the user to whose account the SSH key was added + * @param key the key which was added + */ + public void notifyUserAboutExpiredSshKey(User recipient, UserSshPublicKey key) { + notifyRecipientWithNotificationType(key, SSH_KEY_HAS_EXPIRED, recipient, null); + } + + /** + * Notify user about the addition of a VCS access token + * + * @param recipient the user to whose account the VCS access token was added + */ + public void notifyUserAboutNewlyAddedVcsAccessToken(User recipient) { + notifyRecipientWithNotificationType(null, VCS_ACCESS_TOKEN_ADDED, recipient, null); + } + + /** + * Notify user about the expiration of the VCS access token + * + * @param recipient the user to whose account the VCS access token was added + */ + public void notifyUserAboutExpiredVcsAccessToken(User recipient) { + notifyRecipientWithNotificationType(null, VCS_ACCESS_TOKEN_EXPIRED, recipient, null); + } + + /** + * Notify user about the upcoming expiry of the VCS access token + * + * @param recipient the user to whose account the VCS access token was added + */ + public void notifyUserAboutSoonExpiringVcsAccessToken(User recipient) { + notifyRecipientWithNotificationType(null, VCS_ACCESS_TOKEN_EXPIRES_SOON, recipient, null); + } + /** * Notify student about possible plagiarism case. * diff --git a/src/main/java/de/tum/cit/aet/artemis/core/repository/UserRepository.java b/src/main/java/de/tum/cit/aet/artemis/core/repository/UserRepository.java index c42ca6370ddb..971c4ef9d61a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/repository/UserRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/repository/UserRepository.java @@ -85,6 +85,8 @@ public interface UserRepository extends ArtemisJpaRepository, JpaSpe Optional findOneByEmailIgnoreCase(String email); + List findByVcsAccessTokenExpiryDateBetween(ZonedDateTime from, ZonedDateTime to); + @EntityGraph(type = LOAD, attributePaths = { "groups" }) Optional findOneWithGroupsByEmailIgnoreCase(String email); @@ -595,6 +597,8 @@ default Page searchAllUsersByLoginOrNameInGroupAndConvertToDTO(Pageable @EntityGraph(type = LOAD, attributePaths = { "groups", "authorities" }) Set findAllWithGroupsAndAuthoritiesByIsDeletedIsFalseAndLoginIn(Set logins); + List findAllByIdIn(List ids); + /** * Searches for users by their login or full name. * diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/user/UserService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/user/UserService.java index 4c4fa4e3b3be..dd3a3afd4016 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/user/UserService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/user/UserService.java @@ -67,6 +67,7 @@ import de.tum.cit.aet.artemis.programming.domain.ParticipationVCSAccessToken; import de.tum.cit.aet.artemis.programming.service.ParticipationVcsAccessTokenService; import de.tum.cit.aet.artemis.programming.service.ci.CIUserManagementService; +import de.tum.cit.aet.artemis.programming.service.sshuserkeys.UserSshPublicKeyService; import de.tum.cit.aet.artemis.programming.service.vcs.VcsUserManagementService; import tech.jhipster.security.RandomUtil; @@ -120,11 +121,14 @@ public class UserService { private final SavedPostRepository savedPostRepository; + private final UserSshPublicKeyService userSshPublicKeyService; + public UserService(UserCreationService userCreationService, UserRepository userRepository, AuthorityService authorityService, AuthorityRepository authorityRepository, CacheManager cacheManager, Optional ldapUserService, GuidedTourSettingsRepository guidedTourSettingsRepository, PasswordService passwordService, Optional optionalVcsUserManagementService, Optional optionalCIUserManagementService, InstanceMessageSendService instanceMessageSendService, FileService fileService, Optional scienceEventApi, - ParticipationVcsAccessTokenService participationVCSAccessTokenService, Optional learnerProfileApi, SavedPostRepository savedPostRepository) { + ParticipationVcsAccessTokenService participationVCSAccessTokenService, Optional learnerProfileApi, SavedPostRepository savedPostRepository, + UserSshPublicKeyService userSshPublicKeyService) { this.userCreationService = userCreationService; this.userRepository = userRepository; this.authorityService = authorityService; @@ -141,6 +145,7 @@ public UserService(UserCreationService userCreationService, UserRepository userR this.participationVCSAccessTokenService = participationVCSAccessTokenService; this.learnerProfileApi = learnerProfileApi; this.savedPostRepository = savedPostRepository; + this.userSshPublicKeyService = userSshPublicKeyService; } /** @@ -324,7 +329,6 @@ public User registerUser(UserDTO userDTO, String password) { } catch (VersionControlException e) { log.error("An error occurred while registering GitLab user {}:", savedNonActivatedUser.getLogin(), e); - participationVCSAccessTokenService.deleteAllByUserId(savedNonActivatedUser.getId()); userRepository.delete(savedNonActivatedUser); clearUserCaches(savedNonActivatedUser); userRepository.flush(); @@ -474,6 +478,7 @@ public void softDeleteUser(String login) { userRepository.findOneWithGroupsByLogin(login).ifPresent(user -> { participationVCSAccessTokenService.deleteAllByUserId(user.getId()); learnerProfileApi.ifPresent(api -> api.deleteProfile(user)); + userSshPublicKeyService.deleteAllByUserId(user.getId()); user.setDeleted(true); user.setLearnerProfile(null); anonymizeUser(user); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java index fae3822f08cf..016a38a9bbf6 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java @@ -26,6 +26,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import de.tum.cit.aet.artemis.communication.service.notifications.SingleUserNotificationService; import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.core.dto.PasswordChangeDTO; import de.tum.cit.aet.artemis.core.dto.UserDTO; @@ -66,15 +67,18 @@ public class AccountResource { private final FileService fileService; + private final SingleUserNotificationService singleUserNotificationService; + private static final float MAX_PROFILE_PICTURE_FILESIZE_IN_MEGABYTES = 0.1f; - public AccountResource(UserRepository userRepository, UserService userService, UserCreationService userCreationService, AccountService accountService, - FileService fileService) { + public AccountResource(UserRepository userRepository, UserService userService, UserCreationService userCreationService, AccountService accountService, FileService fileService, + SingleUserNotificationService singleUserNotificationService) { this.userRepository = userRepository; this.userService = userService; this.userCreationService = userCreationService; this.accountService = accountService; this.fileService = fileService; + this.singleUserNotificationService = singleUserNotificationService; } /** @@ -142,6 +146,7 @@ public ResponseEntity createVcsAccessToken(@RequestParam("expiryDate") userRepository.updateUserVcsAccessToken(user.getId(), LocalVCPersonalAccessTokenManagementService.generateSecureVCSAccessToken(), expiryDate); log.debug("Successfully created a VCS access token for user {}", user.getLogin()); + singleUserNotificationService.notifyUserAboutNewlyAddedVcsAccessToken(user); user = userRepository.getUser(); UserDTO userDTO = new UserDTO(); userDTO.setLogin(user.getLogin()); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserSshPublicKeyRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserSshPublicKeyRepository.java index b177b7b72089..fecb35191efe 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserSshPublicKeyRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserSshPublicKeyRepository.java @@ -2,11 +2,14 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; +import java.time.ZonedDateTime; import java.util.List; import java.util.Optional; import org.springframework.context.annotation.Profile; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey; @@ -21,7 +24,13 @@ public interface UserSshPublicKeyRepository extends ArtemisJpaRepository findByIdAndUserId(Long keyId, Long userId); + List findByExpiryDateBetween(ZonedDateTime from, ZonedDateTime to); + boolean existsByIdAndUserId(Long id, Long userId); boolean existsByUserId(Long userId); + + @Transactional // ok because of delete + @Modifying + void deleteAllByUserId(Long userId); } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyExpiryNotificationService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyExpiryNotificationService.java new file mode 100644 index 000000000000..091693934c31 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyExpiryNotificationService.java @@ -0,0 +1,77 @@ +package de.tum.cit.aet.artemis.programming.service.sshuserkeys; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_SCHEDULING; +import static java.time.ZonedDateTime.now; + +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import de.tum.cit.aet.artemis.communication.service.notifications.SingleUserNotificationService; +import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.core.repository.UserRepository; +import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey; +import de.tum.cit.aet.artemis.programming.repository.UserSshPublicKeyRepository; + +@Profile(PROFILE_SCHEDULING) +@Service +public class UserSshPublicKeyExpiryNotificationService { + + private final UserSshPublicKeyRepository userSshPublicKeyRepository; + + private final SingleUserNotificationService singleUserNotificationService; + + private final UserRepository userRepository; + + public UserSshPublicKeyExpiryNotificationService(UserSshPublicKeyRepository userSshPublicKeyRepository, SingleUserNotificationService singleUserNotificationService, + UserRepository userRepository) { + this.userSshPublicKeyRepository = userSshPublicKeyRepository; + this.singleUserNotificationService = singleUserNotificationService; + this.userRepository = userRepository; + } + + /** + * Schedules SSH key expiry notifications to users every morning at 7:00:00 am + */ + @Scheduled(cron = "0 0 7 * * *") + public void sendKeyExpirationNotifications() { + notifyUserOnExpiredKey(); + notifyUserOnUpcomingKeyExpiry(); + } + + /** + * Notifies the user at the day of key expiry, that the key has expired + */ + public void notifyUserOnExpiredKey() { + notifyUsersForKeyExpiryWindow(now().minusDays(1), now(), singleUserNotificationService::notifyUserAboutExpiredSshKey); + } + + /** + * Notifies the user one week in advance about the upcoming expiry + */ + public void notifyUserOnUpcomingKeyExpiry() { + notifyUsersForKeyExpiryWindow(now().plusDays(6), now().plusDays(7), singleUserNotificationService::notifyUserAboutSoonExpiringSshKey); + } + + /** + * Notifies users whose SSH keys are expiring within the specified date range, with the notification specified by the + * notifyFunction + * + * @param fromDate the start of the expiry date range + * @param toDate the end of the expiry date range + * @param notifyFunction a function to handle user notification + */ + private void notifyUsersForKeyExpiryWindow(ZonedDateTime fromDate, ZonedDateTime toDate, BiConsumer notifyFunction) { + var soonExpiringKeys = userSshPublicKeyRepository.findByExpiryDateBetween(fromDate, toDate); + List users = userRepository.findAllByIdIn(soonExpiringKeys.stream().map(UserSshPublicKey::getUserId).toList()); + Map userMap = users.stream().collect(Collectors.toMap(User::getId, Function.identity())); + soonExpiringKeys.forEach(key -> notifyFunction.accept(userMap.get(key.getUserId()), key)); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyService.java similarity index 86% rename from src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java rename to src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyService.java index 232afd327663..fba1cd8cd4f9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyService.java @@ -1,4 +1,4 @@ -package de.tum.cit.aet.artemis.programming.service; +package de.tum.cit.aet.artemis.programming.service.sshuserkeys; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; @@ -15,6 +15,7 @@ import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; +import de.tum.cit.aet.artemis.communication.service.notifications.SingleUserNotificationService; import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException; import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; @@ -29,8 +30,11 @@ public class UserSshPublicKeyService { private final UserSshPublicKeyRepository userSshPublicKeyRepository; - public UserSshPublicKeyService(UserSshPublicKeyRepository userSshPublicKeyRepository) { + private final SingleUserNotificationService singleUserNotificationService; + + public UserSshPublicKeyService(UserSshPublicKeyRepository userSshPublicKeyRepository, SingleUserNotificationService singleUserNotificationService) { this.userSshPublicKeyRepository = userSshPublicKeyRepository; + this.singleUserNotificationService = singleUserNotificationService; } /** @@ -55,8 +59,14 @@ public void createSshKeyForUser(User user, AuthorizedKeyEntry keyEntry, UserSshP newUserSshPublicKey.setKeyHash(keyHash); setLabelForKey(newUserSshPublicKey, sshPublicKey.label()); newUserSshPublicKey.setCreationDate(ZonedDateTime.now()); - newUserSshPublicKey.setExpiryDate(sshPublicKey.expiryDate()); + + if (sshPublicKey.expiryDate() != null) { + var expiryDate = sshPublicKey.expiryDate().withHour(3).withMinute(0).withSecond(0).withNano(0).plusDays(1); + newUserSshPublicKey.setExpiryDate(expiryDate); + } + userSshPublicKeyRepository.save(newUserSshPublicKey); + singleUserNotificationService.notifyUserAboutNewlyAddedSshKey(user, newUserSshPublicKey); } /** @@ -136,4 +146,13 @@ public void deleteUserSshPublicKey(Long userId, Long keyId) { public boolean hasUserSSHkeys(Long userId) { return userSshPublicKeyRepository.existsByUserId(userId); } + + /** + * Deletes all the ssh keys of a user + * + * @param userId the ID of the user. + */ + public void deleteAllByUserId(Long userId) { + userSshPublicKeyRepository.deleteAllByUserId(userId); + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/tokens/UserTokenExpiryNotificationService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/tokens/UserTokenExpiryNotificationService.java new file mode 100644 index 000000000000..1e3622096309 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/tokens/UserTokenExpiryNotificationService.java @@ -0,0 +1,69 @@ +package de.tum.cit.aet.artemis.programming.service.tokens; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_SCHEDULING; +import static java.time.ZonedDateTime.now; + +import java.time.ZonedDateTime; +import java.util.function.Consumer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import de.tum.cit.aet.artemis.communication.service.notifications.SingleUserNotificationService; +import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.core.repository.UserRepository; + +@Profile(PROFILE_SCHEDULING) +@Service +public class UserTokenExpiryNotificationService { + + private static final Logger log = LoggerFactory.getLogger(UserTokenExpiryNotificationService.class); + + private final SingleUserNotificationService singleUserNotificationService; + + private final UserRepository userRepository; + + public UserTokenExpiryNotificationService(SingleUserNotificationService singleUserNotificationService, UserRepository userRepository) { + this.singleUserNotificationService = singleUserNotificationService; + this.userRepository = userRepository; + } + + /** + * Schedules VCS access token expiry notifications to users every morning at 6:00:00 am + */ + @Scheduled(cron = "0 0 6 * * *") + public void sendTokenExpirationNotifications() { + log.info("Sending Token expiration notifications to single user"); + notifyOnExpiredToken(); + notifyUsersOnUpcomingVcsAccessTokenExpiry(); + } + + /** + * Notifies the users at the day of VCS access token expiry + */ + public void notifyOnExpiredToken() { + notifyUsersForKeyExpiryWindow(now().minusDays(1), now(), singleUserNotificationService::notifyUserAboutExpiredVcsAccessToken); + } + + /** + * Notifies the users one week before the VCS access tokens expiry + */ + public void notifyUsersOnUpcomingVcsAccessTokenExpiry() { + notifyUsersForKeyExpiryWindow(now().plusDays(6), now().plusDays(7), singleUserNotificationService::notifyUserAboutSoonExpiringVcsAccessToken); + } + + /** + * Notifies users whose VCS access tokens are expiring within the specified date range, with the notification specified by the + * notifyFunction + * + * @param fromDate the start of the expiry date range + * @param toDate the end of the expiry date range + * @param notifyFunction a function to handle user notification + */ + private void notifyUsersForKeyExpiryWindow(ZonedDateTime fromDate, ZonedDateTime toDate, Consumer notifyFunction) { + userRepository.findByVcsAccessTokenExpiryDateBetween(fromDate, toDate).forEach(notifyFunction); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/localvc/ssh/SshPublicKeysResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/sshuserkeys/SshPublicKeysResource.java similarity index 97% rename from src/main/java/de/tum/cit/aet/artemis/programming/web/localvc/ssh/SshPublicKeysResource.java rename to src/main/java/de/tum/cit/aet/artemis/programming/web/sshuserkeys/SshPublicKeysResource.java index 8eb38fdfd263..cb6da7284b5c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/localvc/ssh/SshPublicKeysResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/sshuserkeys/SshPublicKeysResource.java @@ -1,4 +1,4 @@ -package de.tum.cit.aet.artemis.programming.web.localvc.ssh; +package de.tum.cit.aet.artemis.programming.web.sshuserkeys; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_LOCALVC; @@ -25,7 +25,7 @@ import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey; import de.tum.cit.aet.artemis.programming.dto.UserSshPublicKeyDTO; -import de.tum.cit.aet.artemis.programming.service.UserSshPublicKeyService; +import de.tum.cit.aet.artemis.programming.service.sshuserkeys.UserSshPublicKeyService; @Profile(PROFILE_LOCALVC) @RestController @@ -92,7 +92,6 @@ public ResponseEntity addSshPublicKey(@RequestBody UserSshPublicKeyDTO ssh catch (IllegalArgumentException e) { throw new BadRequestAlertException("Invalid SSH key format", "SSH key", "invalidKeyFormat", true); } - userSshPublicKeyService.createSshKeyForUser(user, keyEntry, sshPublicKey); return ResponseEntity.ok().build(); } diff --git a/src/main/resources/config/liquibase/changelog/20241213144500_changelog.xml b/src/main/resources/config/liquibase/changelog/20241213144500_changelog.xml new file mode 100644 index 000000000000..7d9c8db45fd5 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241213144500_changelog.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index ccbac5f97779..25f6aaac5221 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -1,6 +1,6 @@ + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> @@ -46,6 +46,7 @@ + diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index 812d53c94d69..4fc7d98b153c 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -29,7 +29,7 @@ email.saml.greeting=Dear {0} email.saml.text1=Your Artemis account has been created. A local Artemis password is only needed to access Git and build services. To create your local Artemis password click the link below: email.saml.text2=After expiration of this link you can use the "password-reset" button. email.saml.text3=Regards, -email.saml.username=User name: {0} +email.saml.username=Username: {0} email.saml.email=E-Mail: {0} # Weekly summary email @@ -111,7 +111,7 @@ email.notification.title.tutorialGroup.deleted=The tutorial group {0} in the cou email.notification.title.tutorialGroup.assigned={2} assigned you to lead the tutorial group {0} in the course {1}. email.notification.title.tutorialGroup.unassigned={2} unassigned you from leading the tutorial group {0} in the course {1}. -# Data export +# Data Export email.notification.title.dataExportCreated = Your requested data export has been successfully created and can be downloaded in the next 7 days using the link below. email.notification.title.dataExportFailed = Your requested data export could not be created. email.dataExportFailedAdmin.title = Data export failed for user {0} @@ -119,13 +119,32 @@ email.dataExportFailedAdmin.text = The data export for the user with the login email.dataExportFailedAdmin.textFailed = failed. email.dataExportFailedAdmin.reason = The exception message was the following: {0} email.dataExportFailedAdmin.actionItemList = Please complete the following action items: -email.dataExportFailedAdmin.actionItem1 = \u2022 Make sure the configuration for your Artemis instance is correct. +email.dataExportFailedAdmin.actionItem1 = \u2022 Make sure the configuration for your Artemis instance is correct. email.dataExportFailedAdmin.actionItem2 = \u2022 If you need further help, please contact the Artemis developers by opening an issue on GitHub using the link below: email.dataExportFailedAdmin.githubLink = Link to open an issue on the Artemis GitHub project email.successfulDataExportCreationsAdmin.title = Successfully created requested data exports for your instance -email.successfulDataExportCreationsAdmin.text = Data exports for the following users were successfully created when the data export creation job was ran: +email.successfulDataExportCreationsAdmin.text = Data exports for the following users were successfully created when the data export creation job was running: email.successfulDataExportCreationsAdmin.userLogin = \u2022 {0} +# SSH User Settings +email.notification.sshKeyAdded.title = A new SSH key was added to your account. +email.notification.sshKeyAdded.ifMistake = If you believe this key was added in error, you can remove the key and disable access at the following location: +email.notification.sshKeyAdded.notifyAdmin = If you did not add the SSH key yourself, please report this to your administrator: +email.notification.sshKeyExpiry.sshKeyExpiresSoonWarning = One of your SSH keys will expire in a few days. +email.notification.sshKeyExpiry.sshKeysHasExpiredWarning = One of your SSH keys has expired. +email.notification.sshKeyExpiry.expiryDate = Expiry date: +email.notification.sshKeyExpiry.renew = You can renew your SSH key here: +email.notification.sshKeyExpiry.sshKeyHash = SSH key hash: +email.notification.sshKeyExpiry.sshKeyLabel = SSH key label: + +# VCS Token Settings +email.notification.vcsAccessTokenAdded.title = A new VCS access token was added to your account. +email.notification.vcsAccessTokenAdded.ifMistake = If you believe this token was added in error, you can remove it and disable access at the following location: +email.notification.vcsAccessTokenAdded.notifyAdmin = If you did not add the VCS access token yourself, please report this to your administrator: +email.notification.vcsAccessTokenExpiresSoon.title = Your personal VCS access token expires in one week. +email.notification.vcsAccessTokenExpiry.title = Your personal VCS access token has expired. +email.notification.vcsAccessTokenExpiry.renew = You can renew it here: + # Email Subjects # The reason for the format artemisApp.{notificationCategory}.title.{notificicationType} is that these placeholders are also used in the client and this is the format used there artemisApp.groupNotification.title.attachmentChange = Attachment updated @@ -147,9 +166,14 @@ artemisApp.singleUserNotification.title.tutorialGroupDeregistrationTutor = A stu artemisApp.singleUserNotification.title.tutorialGroupMultipleRegistrationTutor = Multiple students have been registered to your tutorial group artemisApp.singleUserNotification.title.tutorialGroupAssigned = You have been assigned to lead a tutorial group artemisApp.singleUserNotification.title.tutorialGroupUnassigned = You have been unassigned from leading a tutorial group - artemisApp.tutorialGroupNotification.title.tutorialGroupDeleted = Tutorial Group deleted artemisApp.tutorialGroupNotification.title.tutorialGroupUpdated = Tutorial Group updated artemisApp.singleUserNotification.title.dataExportCreated = Your Artemis data export has been successfully created artemisApp.singleUserNotification.title.dataExportFailed = Your Artemis data export could not be created +artemisApp.singleUserNotification.title.sshKeyAdded = New SSH key added to account +artemisApp.singleUserNotification.title.sshKeyExpiresSoon = SSH key expires soon +artemisApp.singleUserNotification.title.sshKeyHasExpired = SSH key has expired +artemisApp.singleUserNotification.title.vcsAccessTokenAdded = New VCS access token added to account +artemisApp.singleUserNotification.title.vcsAccessTokenExpired = VCS access token has expired +artemisApp.singleUserNotification.title.vcsAccessTokenExpiresSoon = VCS access expires soon diff --git a/src/main/resources/i18n/messages_de.properties b/src/main/resources/i18n/messages_de.properties index 66c10bb22527..354b55904e49 100644 --- a/src/main/resources/i18n/messages_de.properties +++ b/src/main/resources/i18n/messages_de.properties @@ -93,7 +93,7 @@ email.notification.aux.difficulty.hard=Schwer # Plagiarism email.plagiarism.title=Neuer Plagiatsfall: Übung "{0}" im Kurs "{1}" -email.plagiarism.cpc.title=Neue signifikante ?bereinstimmung: Aufgabe "{0}" im Kurs "{1}" +email.plagiarism.cpc.title=Neue signifikante Übereinstimmung: Aufgabe "{0}" im Kurs "{1}" email.notification.title.post.plagiarismVerdict=Entscheidung zum Plagiatsfall in der Aufgabe {0} gefallen email.notification.aux.plagiarismVerdict.plagiarism=Der Fall wird als Plagiat angesehen! email.notification.aux.plagiarismVerdict.point.deduction=Wegen des Plagiatsfalls ziehen wir dir Punkte in der Aufgabe ab! @@ -125,6 +125,26 @@ email.dataExportFailedAdmin.githubLink = Link um ein Issue im Artemis GitHub Pro email.successfulDataExportCreationsAdmin.title = Angeforderte Datenexporte wurden f?r deine Instanz erfolgreich erstellt email.successfulDataExportCreationsAdmin.text = Datenexporte f?r die folgenden Nutzer wurden erfolgreich erstellt als der Job um die Datenexporte zu erstellen zuletzt ausgef?hrt wurde: email.successfulDataExportCreationsAdmin.userLogin = \u2022 {0} + +# SSH User Settings +email.notification.sshKeyAdded.title = Ein neuer SSH-Schlüssel wurde zu deinem Benutzerkonto hinzugefügt. +email.notification.sshKeyAdded.ifMistake = Wenn du glaubst, dass dieser Schlüssel irrtümlich hinzugefügt wurde, kannst du den Schlüssel entfernen und den Zugriff an folgender Stelle deaktivieren: +email.notification.sshKeyAdded.notifyAdmin = Wenn du diesen Schlüssel nicht selbst hinzugefügt hast, melde das bitte deinem Administrator: +email.notification.sshKeyExpiry.sshKeyExpiresSoonWarning = Einer deiner SSH-Schlüssel läuft in wenigen Tagen ab. +email.notification.sshKeyExpiry.sshKeysHasExpiredWarning = Einer deiner SSH-Schlüssel ist abgelaufen. +email.notification.sshKeyExpiry.expiryDate = Ablaufdatum: +email.notification.sshKeyExpiry.renew = Hier kannst du deinen SSH-Schlüssel aktualisieren: +email.notification.sshKeyExpiry.sshKeyHash = SSH-Schlüssel-Hash: +email.notification.sshKeyExpiry.sshKeyLabel = SSH-Schlüssel-Label: + +# VCS access token User Settings +email.notification.vcsAccessTokenAdded.title = Ein neues VCS Zugriffstoken wurde zu deinem Benutzerkonto hinzugefügt. +email.notification.vcsAccessTokenAdded.ifMistake = Wenn du glaubst, dass dieses Zugriffstoken irrtümlich hinzugefügt wurde, kannst du es entfernen und den Zugriff an folgender Stelle deaktivieren: +email.notification.vcsAccessTokenAdded.notifyAdmin = Wenn du dieses VCS Zugriffstoken nicht selbst hinzugefügt hast, melde das bitte deinem Administrator: +email.notification.vcsAccessTokenExpiresSoon.title = Dein persönliches VCS Zugriffstoken läuft in einer Woche ab. +email.notification.vcsAccessTokenExpiry.title = Dein persönliches VCS Zugriffstoken ist abgelaufen. +email.notification.vcsAccessTokenExpiry.renew = Du kannst es hier erneuern: + # Email Subjects # The reason for the format artemisApp.{notificationCategory}.title.{notificicationType} is that these placeholders are also used in the client and this is the format used there artemisApp.groupNotification.title.attachmentChange = Anhang aktualisiert @@ -152,3 +172,9 @@ artemisApp.tutorialGroupNotification.title.tutorialGroupUpdated = Übungsgruppe artemisApp.singleUserNotification.title.dataExportCreated = Dein Artemis Datenexport wurde erfolgreich erstellt artemisApp.singleUserNotification.title.dataExportFailed = Dein Artemis Datenexport konnte nicht erstellt werden +artemisApp.singleUserNotification.title.sshKeyAdded = Neuer SSH-Schlüssel hinzugefügt +artemisApp.singleUserNotification.title.sshKeyExpiresSoon = SSH-Schlüssel läuft bald ab +artemisApp.singleUserNotification.title.sshKeyHasExpired = SSH-Schlüssel ist abgelaufen +artemisApp.singleUserNotification.title.vcsAccessTokenAdded = Neues VCS Zugriffstoken hinzugefügt +artemisApp.singleUserNotification.title.vcsAccessTokenExpired = VCS Zugriffstoken ist abgelaufen +artemisApp.singleUserNotification.title.vcsAccessTokenExpiresSoon = VCS Zugriffstoken läuft bald ab diff --git a/src/main/resources/i18n/messages_en.properties b/src/main/resources/i18n/messages_en.properties index c209a8cf133d..4fc7d98b153c 100644 --- a/src/main/resources/i18n/messages_en.properties +++ b/src/main/resources/i18n/messages_en.properties @@ -126,6 +126,25 @@ email.successfulDataExportCreationsAdmin.title = Successfully created requested email.successfulDataExportCreationsAdmin.text = Data exports for the following users were successfully created when the data export creation job was running: email.successfulDataExportCreationsAdmin.userLogin = \u2022 {0} +# SSH User Settings +email.notification.sshKeyAdded.title = A new SSH key was added to your account. +email.notification.sshKeyAdded.ifMistake = If you believe this key was added in error, you can remove the key and disable access at the following location: +email.notification.sshKeyAdded.notifyAdmin = If you did not add the SSH key yourself, please report this to your administrator: +email.notification.sshKeyExpiry.sshKeyExpiresSoonWarning = One of your SSH keys will expire in a few days. +email.notification.sshKeyExpiry.sshKeysHasExpiredWarning = One of your SSH keys has expired. +email.notification.sshKeyExpiry.expiryDate = Expiry date: +email.notification.sshKeyExpiry.renew = You can renew your SSH key here: +email.notification.sshKeyExpiry.sshKeyHash = SSH key hash: +email.notification.sshKeyExpiry.sshKeyLabel = SSH key label: + +# VCS Token Settings +email.notification.vcsAccessTokenAdded.title = A new VCS access token was added to your account. +email.notification.vcsAccessTokenAdded.ifMistake = If you believe this token was added in error, you can remove it and disable access at the following location: +email.notification.vcsAccessTokenAdded.notifyAdmin = If you did not add the VCS access token yourself, please report this to your administrator: +email.notification.vcsAccessTokenExpiresSoon.title = Your personal VCS access token expires in one week. +email.notification.vcsAccessTokenExpiry.title = Your personal VCS access token has expired. +email.notification.vcsAccessTokenExpiry.renew = You can renew it here: + # Email Subjects # The reason for the format artemisApp.{notificationCategory}.title.{notificicationType} is that these placeholders are also used in the client and this is the format used there artemisApp.groupNotification.title.attachmentChange = Attachment updated @@ -152,3 +171,9 @@ artemisApp.tutorialGroupNotification.title.tutorialGroupUpdated = Tutorial Group artemisApp.singleUserNotification.title.dataExportCreated = Your Artemis data export has been successfully created artemisApp.singleUserNotification.title.dataExportFailed = Your Artemis data export could not be created +artemisApp.singleUserNotification.title.sshKeyAdded = New SSH key added to account +artemisApp.singleUserNotification.title.sshKeyExpiresSoon = SSH key expires soon +artemisApp.singleUserNotification.title.sshKeyHasExpired = SSH key has expired +artemisApp.singleUserNotification.title.vcsAccessTokenAdded = New VCS access token added to account +artemisApp.singleUserNotification.title.vcsAccessTokenExpired = VCS access token has expired +artemisApp.singleUserNotification.title.vcsAccessTokenExpiresSoon = VCS access expires soon diff --git a/src/main/resources/templates/mail/notification/sshKeyAddedEmail.html b/src/main/resources/templates/mail/notification/sshKeyAddedEmail.html new file mode 100644 index 000000000000..a0f566a840e3 --- /dev/null +++ b/src/main/resources/templates/mail/notification/sshKeyAddedEmail.html @@ -0,0 +1,42 @@ + + + + + + + + + +
+ + +

+ The following SSH key was added to your account: +

+

+ SSH key label + SSH key label +

+

+ SSH key hash + SSH key hash +

+

+

+ If you believe this key was added in error, you can remove the key and disable access at the following location + + Login + link +

+ +

+ If you did not add the ssh key yourself, please report this to your administrator: + + +

+ + +
+ + + diff --git a/src/main/resources/templates/mail/notification/sshKeyExpiresSoonEmail.html b/src/main/resources/templates/mail/notification/sshKeyExpiresSoonEmail.html new file mode 100644 index 000000000000..2d95c31061f1 --- /dev/null +++ b/src/main/resources/templates/mail/notification/sshKeyExpiresSoonEmail.html @@ -0,0 +1,40 @@ + + + + + + + + + +
+ + +

+ One of your SSH keys will expire in a few days: +

+

+ SSH key label + SSH key label +

+

+ SSH key hash + SSH key hash +

+

+ + +

+

+

+ You can renew your SSH key here: + + Login + link +

+ + +
+ + + diff --git a/src/main/resources/templates/mail/notification/sshKeyHasExpiredEmail.html b/src/main/resources/templates/mail/notification/sshKeyHasExpiredEmail.html new file mode 100644 index 000000000000..f500ce20ba18 --- /dev/null +++ b/src/main/resources/templates/mail/notification/sshKeyHasExpiredEmail.html @@ -0,0 +1,40 @@ + + + + + + + + + +
+ + +

+ One of your SSH keys has expired. +

+

+ SSH key label + SSH key label +

+

+ SSH key hash + SSH key hash +

+

+ + +

+

+

+ You can renew your SSH key here: + + Login + link +

+ + +
+ + + diff --git a/src/main/resources/templates/mail/notification/vcsAccessTokenAddedEmail.html b/src/main/resources/templates/mail/notification/vcsAccessTokenAddedEmail.html new file mode 100644 index 000000000000..8708c9be85fe --- /dev/null +++ b/src/main/resources/templates/mail/notification/vcsAccessTokenAddedEmail.html @@ -0,0 +1,34 @@ + + + + + + + + + +
+ + +

+ The following VCS access token was added to your account: +

+ +

+ If you believe this token was added in error, you can remove it and disable access at the following location: + + Login + link +

+ +

+ If you did not add the VCS access token yourself, please report this to your administrator: + + +

+ + +
+ + + diff --git a/src/main/resources/templates/mail/notification/vcsAccessTokenExpiredEmail.html b/src/main/resources/templates/mail/notification/vcsAccessTokenExpiredEmail.html new file mode 100644 index 000000000000..2b4f497145ad --- /dev/null +++ b/src/main/resources/templates/mail/notification/vcsAccessTokenExpiredEmail.html @@ -0,0 +1,28 @@ + + + + + + + + + +
+ + +

+ Your VCS access token has expired +

+ +

+ You can renew it here + + Login + link +

+ + +
+ + + diff --git a/src/main/resources/templates/mail/notification/vcsAccessTokenExpiresSoonEmail.html b/src/main/resources/templates/mail/notification/vcsAccessTokenExpiresSoonEmail.html new file mode 100644 index 000000000000..e27ebab19839 --- /dev/null +++ b/src/main/resources/templates/mail/notification/vcsAccessTokenExpiresSoonEmail.html @@ -0,0 +1,28 @@ + + + + + + + + + +
+ + +

+ Your VCS access token will expire in one week +

+ +

+ You can renew it here + + Login + link +

+ + +
+ + + diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html index 5554d80077e8..57cb54c7d323 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html @@ -72,6 +72,7 @@

} @if (displayedExpiryDate) {
-
+
{{ displayedExpiryDate | artemisDate: 'long-date' }}
diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts index bdda46011677..f22d8acc75c8 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts @@ -13,6 +13,7 @@ import { getOS } from 'app/shared/util/os-detector.util'; import { UserSshPublicKey } from 'app/entities/programming/user-ssh-public-key.model'; import dayjs from 'dayjs/esm'; import { SshUserSettingsService } from 'app/shared/user-settings/ssh-settings/ssh-user-settings.service'; +import { DateTimePickerType } from 'app/shared/date-time-picker/date-time-picker.component'; import { FormsModule } from '@angular/forms'; import { ArtemisDatePipe } from 'app/shared/pipes/artemis-date.pipe'; @@ -51,6 +52,7 @@ export class SshUserSettingsKeyDetailsComponent implements OnInit, OnDestroy { displayedKeyLabel = ''; displayedSshKey = ''; displayedKeyHash = ''; + hasExpired? = false; displayedExpiryDate?: dayjs.Dayjs; isExpiryDateValid = false; displayCreationDate: dayjs.Dayjs; @@ -87,6 +89,7 @@ export class SshUserSettingsKeyDetailsComponent implements OnInit, OnDestroy { this.displayCreationDate = publicKey.creationDate; this.displayedExpiryDate = publicKey.expiryDate; this.displayedLastUsedDate = publicKey.lastUsedDate; + this.hasExpired = publicKey.expiryDate && dayjs().isAfter(dayjs(publicKey.expiryDate)); this.isLoading = false; }), ) @@ -145,4 +148,6 @@ export class SshUserSettingsKeyDetailsComponent implements OnInit, OnDestroy { this.copyInstructions = 'Ctrl + C'; } } + + protected readonly DateTimePickerType = DateTimePickerType; } diff --git a/src/main/webapp/i18n/de/notification.json b/src/main/webapp/i18n/de/notification.json index 8b99a44aa5f7..efb3c213f7e2 100644 --- a/src/main/webapp/i18n/de/notification.json +++ b/src/main/webapp/i18n/de/notification.json @@ -124,7 +124,13 @@ "addUserGroupChat": "Du wurdest zu einem Gruppenchat hinzugefügt", "createGroupChat": "Neuer Gruppenchat", "dataExportCreated": "Datenexport erstellt", - "dataExportFailed": "Datenexport fehlgeschlagen" + "dataExportFailed": "Datenexport fehlgeschlagen", + "sshKeyAdded": "Neuer SSH-Schlüssel hinzugefügt", + "sshKeyExpiresSoon": "SSH-Schlüssel läuft bald ab", + "sshKeyHasExpired": "SSH-Schlüssel ist abgelaufen", + "vcsAccessTokenAdded": "Neues VCS Zugriffstoken hinzugefügt", + "vcsAccessTokenExpired": "VCS Zugriffstoken abgelaufen", + "vcsAccessTokenExpiresSoon": "VCS Zugriffstoken läuft bald ab" }, "text": { "newReplyForExercisePost": "Auf deinen Beitrag zur Aufgabe \"{{ placeholderValues.8 }}\" im Kurs \"{{ placeholderValues.0 }}\" wurde geantwortet: \"{{ placeholderValues.5 }}\"", @@ -152,7 +158,13 @@ "addUserGroupChat": "Du wurdest von {{ placeholderValues.1 }} zu einem neuen Gruppenchat im Kurs {{ placeholderValues.0 }} hinzugefügt.", "createGroupChat": "Du wurdest von {{ placeholderValues.1 }} zu einem neuen Gruppenchat im Kurs {{ placeholderValues.0 }} hinzugefügt.", "dataExportCreated": "Dein Datenexport wurde erstellt und kann nun heruntergeladen werden.", - "dataExportFailed": "Dein Datenexport konnte nicht erstellt werden. Bitte versuche es später erneut." + "dataExportFailed": "Dein Datenexport konnte nicht erstellt werden. Bitte versuche es später erneut.", + "sshKeyAdded": "Du hast erfolgreich einen SSH-Schlüssel hinzugefügt", + "sshKeyExpiresSoon": "Dein SSH-Schlüssel mit dem Label \"{{ placeholderValues.0 }}\" läuft am {{ placeholderValues.1 }} ab.", + "sshKeyHasExpired": "Dein SSH-Schlüssel mit dem Label \"{{ placeholderValues.0 }}\" ist am {{ placeholderValues.1 }} abgelaufen.", + "vcsAccessTokenAdded": "Du hast erfolgreich ein persönliches Zugriffstoken hinzugefügt.", + "vcsAccessTokenExpired": "Dein persönliches Zugriffstoken ist abgelaufen.", + "vcsAccessTokenExpiresSoon": "Dein persönliches Zugriffstoken läuft bald ab." } }, "tutorialGroupNotification": { diff --git a/src/main/webapp/i18n/en/notification.json b/src/main/webapp/i18n/en/notification.json index 52a7e61ae26e..7a874940c473 100644 --- a/src/main/webapp/i18n/en/notification.json +++ b/src/main/webapp/i18n/en/notification.json @@ -124,7 +124,13 @@ "addUserGroupChat": "You have been added to a group chat", "createGroupChat": "New group chat", "dataExportCreated": "Data export created", - "dataExportFailed": "Data export failed" + "dataExportFailed": "Data export failed", + "sshKeyAdded": "New SSH key added", + "sshKeyExpiresSoon": "SSH key will expire soon", + "sshKeyHasExpired": "SSH key has expired", + "vcsAccessTokenAdded": "New VCS access token added", + "vcsAccessTokenExpired": "VCS access token has expired", + "vcsAccessTokenExpiresSoon": "VCS access token will expire soon" }, "text": { "newReplyForExercisePost": "Your post regarding exercise \"{{ placeholderValues.8 }}\" in the course \"{{ placeholderValues.0 }}\" got a new reply: \"{{ placeholderValues.5 }}\"", @@ -152,7 +158,13 @@ "addUserGroupChat": "You have been added to a new group chat by {{ placeholderValues.1 }} in course {{ placeholderValues.0 }}.", "createGroupChat": "You have been added to a new group chat by {{ placeholderValues.1 }} in course {{ placeholderValues.0 }}.", "dataExportCreated": "Your data export has been created and can be downloaded.", - "dataExportFailed": "Your data export could not be created. Please try again later." + "dataExportFailed": "Your data export could not be created. Please try again later.", + "sshKeyAdded": "You have successfully added a new SSH key", + "sshKeyExpiresSoon": "Your SSH key with the label \"{{ placeholderValues.0 }}\" will expire on {{ placeholderValues.1 }}.", + "sshKeyHasExpired": "Your SSH key with the label \"{{ placeholderValues.0 }}\" has expired on {{ placeholderValues.1 }}.", + "vcsAccessTokenAdded": "You have successfully added a new VCS access token", + "vcsAccessTokenExpired": "Your personal version control access token has expired.", + "vcsAccessTokenExpiresSoon": "Your personal version control access token will expire in one week." } }, "tutorialGroupNotification": { diff --git a/src/test/java/de/tum/cit/aet/artemis/communication/notification/GroupNotificationServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/communication/notification/GroupNotificationServiceTest.java index 4ccf0116d463..2f109cb2d32e 100644 --- a/src/test/java/de/tum/cit/aet/artemis/communication/notification/GroupNotificationServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/communication/notification/GroupNotificationServiceTest.java @@ -48,11 +48,11 @@ import de.tum.cit.aet.artemis.communication.domain.Post; import de.tum.cit.aet.artemis.communication.domain.conversation.Channel; import de.tum.cit.aet.artemis.communication.domain.notification.Notification; -import de.tum.cit.aet.artemis.communication.repository.NotificationRepository; import de.tum.cit.aet.artemis.communication.repository.NotificationSettingRepository; import de.tum.cit.aet.artemis.communication.service.notifications.GroupNotificationScheduleService; 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.test_repository.NotificationTestRepository; import de.tum.cit.aet.artemis.core.test_repository.UserTestRepository; import de.tum.cit.aet.artemis.core.user.util.UserUtilService; import de.tum.cit.aet.artemis.core.util.CourseUtilService; @@ -76,7 +76,7 @@ class GroupNotificationServiceTest extends AbstractSpringIntegrationIndependentT private static final String TEST_PREFIX = "groupnotificationservice"; @Autowired - private NotificationRepository notificationRepository; + private NotificationTestRepository notificationTestRepository; @Autowired private NotificationSettingRepository notificationSettingRepository; @@ -231,12 +231,12 @@ void setUp() { userUtilService.changeUser(TEST_PREFIX + "instructor1"); // store the current notification count to let tests work even if notifications are created in other tests - notificationCountBeforeTest = notificationRepository.findAll().size(); + notificationCountBeforeTest = notificationTestRepository.findAll().size(); } @AfterEach void tearDown() { - notificationRepository.deleteAllInBatch(); + notificationTestRepository.deleteAllInBatch(); } /** @@ -262,10 +262,10 @@ private Notification verifyRepositoryCallWithCorrectNotificationAndReturnNotific */ private Notification verifyRepositoryCallWithCorrectNotificationAndReturnNotificationAtIndex(int numberOfGroupsAndCalls, String expectedNotificationTitle, int index) { await().untilAsserted( - () -> assertThat(notificationRepository.findAll()).as("The number of created notifications should be the same as the number of notified groups/authorities") + () -> assertThat(notificationTestRepository.findAll()).as("The number of created notifications should be the same as the number of notified groups/authorities") .hasSize(numberOfGroupsAndCalls + notificationCountBeforeTest)); - List capturedNotifications = notificationRepository.findAll(); + List capturedNotifications = notificationTestRepository.findAll(); Notification lastCapturedNotification = capturedNotifications.get(capturedNotifications.size() - 1); assertThat(lastCapturedNotification.getTitle()).as("The title of the captured notification should be equal to the expected one").isEqualTo(expectedNotificationTitle); @@ -440,10 +440,10 @@ private void verifyPush(Notification notification, Set users, Object notif */ @Test void testNotifyStudentGroupAboutAttachmentChange_futureReleaseDate() { - var countBefore = notificationRepository.count(); + var countBefore = notificationTestRepository.count(); attachment.setReleaseDate(FUTURE_TIME); groupNotificationService.notifyStudentGroupAboutAttachmentChange(attachment, NOTIFICATION_TEXT); - var countAfter = notificationRepository.count(); + var countAfter = notificationTestRepository.count(); assertThat(countAfter).as("No notification should be created/saved").isEqualTo(countBefore); } diff --git a/src/test/java/de/tum/cit/aet/artemis/communication/notification/NotificationResourceIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/communication/notification/NotificationResourceIntegrationTest.java index 07c00a1b9a3a..d01fd96749fd 100644 --- a/src/test/java/de/tum/cit/aet/artemis/communication/notification/NotificationResourceIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/communication/notification/NotificationResourceIntegrationTest.java @@ -20,17 +20,17 @@ import de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants; import de.tum.cit.aet.artemis.communication.domain.notification.SingleUserNotification; import de.tum.cit.aet.artemis.communication.notification.util.NotificationFactory; -import de.tum.cit.aet.artemis.communication.repository.NotificationRepository; import de.tum.cit.aet.artemis.communication.repository.NotificationSettingRepository; 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.test_repository.NotificationTestRepository; import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; import de.tum.cit.aet.artemis.text.util.TextExerciseUtilService; class NotificationResourceIntegrationTest extends AbstractSpringIntegrationIndependentTest { @Autowired - private NotificationRepository notificationRepository; + private NotificationTestRepository notificationTestRepository; @Autowired private NotificationSettingRepository notificationSettingRepository; @@ -49,7 +49,7 @@ void initTestCase() { userUtilService.addUsers(TEST_PREFIX, 2, 1, 1, 1); course1 = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); course2 = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - notificationRepository.deleteAll(); + notificationTestRepository.deleteAll(); User student1 = userUtilService.getUserByLogin(TEST_PREFIX + "student1"); student1.setLastNotificationRead(ZonedDateTime.now().minusDays(1)); @@ -58,7 +58,7 @@ void initTestCase() { @AfterEach void tearDown() { - notificationRepository.deleteAll(); + notificationTestRepository.deleteAll(); } @Test @@ -66,9 +66,9 @@ void tearDown() { void testGetNotifications_recipientEvaluation() throws Exception { User recipient = userTestRepository.getUser(); SingleUserNotification notification1 = NotificationFactory.generateSingleUserNotification(ZonedDateTime.now(), recipient); - notificationRepository.save(notification1); + notificationTestRepository.save(notification1); SingleUserNotification notification2 = NotificationFactory.generateSingleUserNotification(ZonedDateTime.now(), userUtilService.getUserByLogin(TEST_PREFIX + "student2")); - notificationRepository.save(notification2); + notificationTestRepository.save(notification2); List notifications = request.getList("/api/notifications", HttpStatus.OK, Notification.class); assertThat(notifications).as("Notification with recipient equal to current user is returned").contains(notification1); @@ -81,11 +81,11 @@ void testGetNotifications_courseEvaluation() throws Exception { // student1 is member of `testgroup` and `tumuser` per default // the studentGroupName of course1 is `tumuser` per default GroupNotification notification1 = NotificationFactory.generateGroupNotification(ZonedDateTime.now(), course1, GroupNotificationType.STUDENT); - notificationRepository.save(notification1); + notificationTestRepository.save(notification1); course2.setStudentGroupName("some-group"); courseRepository.save(course2); GroupNotification notification2 = NotificationFactory.generateGroupNotification(ZonedDateTime.now(), course2, GroupNotificationType.STUDENT); - notificationRepository.save(notification2); + notificationTestRepository.save(notification2); List notifications = request.getList("/api/notifications", HttpStatus.OK, Notification.class); assertThat(notifications).as("Notification with course the current user belongs to is returned").contains(notification1); @@ -96,13 +96,13 @@ void testGetNotifications_courseEvaluation() throws Exception { @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void testGetNotifications_groupNotificationTypeEvaluation_asStudent() throws Exception { GroupNotification notificationStudent = NotificationFactory.generateGroupNotification(ZonedDateTime.now(), course1, GroupNotificationType.STUDENT); - notificationRepository.save(notificationStudent); + notificationTestRepository.save(notificationStudent); GroupNotification notificationTutor = NotificationFactory.generateGroupNotification(ZonedDateTime.now(), course1, GroupNotificationType.TA); - notificationRepository.save(notificationTutor); + notificationTestRepository.save(notificationTutor); GroupNotification notificationEditor = NotificationFactory.generateGroupNotification(ZonedDateTime.now(), course1, GroupNotificationType.EDITOR); - notificationRepository.save(notificationEditor); + notificationTestRepository.save(notificationEditor); GroupNotification notificationInstructor = NotificationFactory.generateGroupNotification(ZonedDateTime.now(), course1, GroupNotificationType.INSTRUCTOR); - notificationRepository.save(notificationInstructor); + notificationTestRepository.save(notificationInstructor); List notifications = request.getList("/api/notifications", HttpStatus.OK, Notification.class); assertThat(notifications).as("Notification with type student is returned").contains(notificationStudent); @@ -115,13 +115,13 @@ void testGetNotifications_groupNotificationTypeEvaluation_asStudent() throws Exc @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") void testGetNotifications_groupNotificationTypeEvaluation_asTutor() throws Exception { GroupNotification notificationStudent = NotificationFactory.generateGroupNotification(ZonedDateTime.now(), course1, GroupNotificationType.STUDENT); - notificationRepository.save(notificationStudent); + notificationTestRepository.save(notificationStudent); GroupNotification notificationTutor = NotificationFactory.generateGroupNotification(ZonedDateTime.now(), course1, GroupNotificationType.TA); - notificationRepository.save(notificationTutor); + notificationTestRepository.save(notificationTutor); GroupNotification notificationEditor = NotificationFactory.generateGroupNotification(ZonedDateTime.now(), course1, GroupNotificationType.EDITOR); - notificationRepository.save(notificationEditor); + notificationTestRepository.save(notificationEditor); GroupNotification notificationInstructor = NotificationFactory.generateGroupNotification(ZonedDateTime.now(), course1, GroupNotificationType.INSTRUCTOR); - notificationRepository.save(notificationInstructor); + notificationTestRepository.save(notificationInstructor); List notifications = request.getList("/api/notifications", HttpStatus.OK, Notification.class); assertThat(notifications).as("Notification with type student is not returned").doesNotContain(notificationStudent); @@ -134,13 +134,13 @@ void testGetNotifications_groupNotificationTypeEvaluation_asTutor() throws Excep @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") void testGetNotifications_groupNotificationTypeEvaluation_asEditor() throws Exception { GroupNotification notificationStudent = NotificationFactory.generateGroupNotification(ZonedDateTime.now(), course1, GroupNotificationType.STUDENT); - notificationRepository.save(notificationStudent); + notificationTestRepository.save(notificationStudent); GroupNotification notificationTutor = NotificationFactory.generateGroupNotification(ZonedDateTime.now(), course1, GroupNotificationType.TA); - notificationRepository.save(notificationTutor); + notificationTestRepository.save(notificationTutor); GroupNotification notificationEditor = NotificationFactory.generateGroupNotification(ZonedDateTime.now(), course1, GroupNotificationType.EDITOR); - notificationRepository.save(notificationEditor); + notificationTestRepository.save(notificationEditor); GroupNotification notificationInstructor = NotificationFactory.generateGroupNotification(ZonedDateTime.now(), course1, GroupNotificationType.INSTRUCTOR); - notificationRepository.save(notificationInstructor); + notificationTestRepository.save(notificationInstructor); List notifications = request.getList("/api/notifications", HttpStatus.OK, Notification.class); assertThat(notifications).as("Notification with type student is not returned").doesNotContain(notificationStudent); @@ -153,13 +153,13 @@ void testGetNotifications_groupNotificationTypeEvaluation_asEditor() throws Exce @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testGetNotifications_groupNotificationTypeEvaluation_asInstructor() throws Exception { GroupNotification notificationStudent = NotificationFactory.generateGroupNotification(ZonedDateTime.now(), course1, GroupNotificationType.STUDENT); - notificationRepository.save(notificationStudent); + notificationTestRepository.save(notificationStudent); GroupNotification notificationTutor = NotificationFactory.generateGroupNotification(ZonedDateTime.now(), course1, GroupNotificationType.TA); - notificationRepository.save(notificationTutor); + notificationTestRepository.save(notificationTutor); GroupNotification notificationEditor = NotificationFactory.generateGroupNotification(ZonedDateTime.now(), course1, GroupNotificationType.EDITOR); - notificationRepository.save(notificationEditor); + notificationTestRepository.save(notificationEditor); GroupNotification notificationInstructor = NotificationFactory.generateGroupNotification(ZonedDateTime.now(), course1, GroupNotificationType.INSTRUCTOR); - notificationRepository.save(notificationInstructor); + notificationTestRepository.save(notificationInstructor); List notifications = request.getList("/api/notifications", HttpStatus.OK, Notification.class); assertThat(notifications).as("Notification with type student is not returned").doesNotContain(notificationStudent); @@ -184,11 +184,11 @@ void testGetAllNotificationsForCurrentUserFilteredBySettings() throws Exception GroupNotification allowedNotification = NotificationFactory.generateGroupNotification(ZonedDateTime.now(), course1, GroupNotificationType.STUDENT); allowedNotification.setTitle(NotificationConstants.findCorrespondingNotificationTitle(allowedType)); - notificationRepository.save(allowedNotification); + notificationTestRepository.save(allowedNotification); GroupNotification blockedNotification = NotificationFactory.generateGroupNotification(ZonedDateTime.now(), course1, GroupNotificationType.STUDENT); blockedNotification.setTitle(NotificationConstants.findCorrespondingNotificationTitle(blockedType)); - notificationRepository.save(blockedNotification); + notificationTestRepository.save(blockedNotification); List notifications = request.getList("/api/notifications", HttpStatus.OK, Notification.class); @@ -207,10 +207,10 @@ void testGetAllNotificationsForCurrentUser_hideUntilDeactivated() throws Excepti userTestRepository.save(student1); GroupNotification futureNotification = NotificationFactory.generateGroupNotification(timeNow.plusHours(1), course1, GroupNotificationType.STUDENT); - notificationRepository.save(futureNotification); + notificationTestRepository.save(futureNotification); GroupNotification pastNotification = NotificationFactory.generateGroupNotification(timeNow.minusHours(1), course1, GroupNotificationType.STUDENT); - notificationRepository.save(pastNotification); + notificationTestRepository.save(pastNotification); List notifications = request.getList("/api/notifications", HttpStatus.OK, Notification.class); @@ -228,10 +228,10 @@ void testGetAllNotificationsForCurrentUser_hideUntilActivated() throws Exception userTestRepository.save(student1); GroupNotification futureNotification = NotificationFactory.generateGroupNotification(timeNow.plusHours(1), course1, GroupNotificationType.STUDENT); - notificationRepository.save(futureNotification); + notificationTestRepository.save(futureNotification); GroupNotification pastNotification = NotificationFactory.generateGroupNotification(timeNow.minusHours(1), course1, GroupNotificationType.STUDENT); - notificationRepository.save(pastNotification); + notificationTestRepository.save(pastNotification); List notifications = request.getList("/api/notifications", HttpStatus.OK, Notification.class); diff --git a/src/test/java/de/tum/cit/aet/artemis/communication/notification/NotificationScheduleServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/communication/notification/NotificationScheduleServiceTest.java index c49163742ba4..a7506c413281 100644 --- a/src/test/java/de/tum/cit/aet/artemis/communication/notification/NotificationScheduleServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/communication/notification/NotificationScheduleServiceTest.java @@ -22,11 +22,11 @@ import de.tum.cit.aet.artemis.assessment.domain.Result; import de.tum.cit.aet.artemis.assessment.test_repository.ResultTestRepository; import de.tum.cit.aet.artemis.communication.domain.NotificationSetting; -import de.tum.cit.aet.artemis.communication.repository.NotificationRepository; import de.tum.cit.aet.artemis.communication.repository.NotificationSettingRepository; 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.service.messaging.InstanceMessageReceiveService; +import de.tum.cit.aet.artemis.core.test_repository.NotificationTestRepository; import de.tum.cit.aet.artemis.core.user.util.UserUtilService; import de.tum.cit.aet.artemis.core.util.CourseUtilService; import de.tum.cit.aet.artemis.exercise.domain.Exercise; @@ -43,7 +43,7 @@ class NotificationScheduleServiceTest extends AbstractSpringIntegrationLocalCILo private InstanceMessageReceiveService instanceMessageReceiveService; @Autowired - private NotificationRepository notificationRepository; + private NotificationTestRepository notificationTestRepository; @Autowired private NotificationSettingRepository notificationSettingRepository; @@ -80,7 +80,7 @@ void init() { exercise.setMaxPoints(5.0); exerciseRepository.saveAndFlush(exercise); - sizeBefore = notificationRepository.count(); + sizeBefore = notificationTestRepository.count(); } @Test @@ -91,7 +91,7 @@ void shouldCreateNotificationAndEmailAtReleaseDate() { exerciseRepository.saveAndFlush(exercise); instanceMessageReceiveService.processScheduleExerciseReleasedNotification(exercise.getId()); - await().until(() -> notificationRepository.count() > sizeBefore); + await().until(() -> notificationTestRepository.count() > sizeBefore); verify(groupNotificationService, timeout(TIMEOUT_MS)).notifyAllGroupsAboutReleasedExercise(exercise); verify(mailService, timeout(TIMEOUT_MS).atLeastOnce()).sendNotification(any(), anySet(), any()); } @@ -114,7 +114,7 @@ void shouldCreateNotificationAndEmailAtAssessmentDueDate() { exerciseRepository.saveAndFlush(exercise); instanceMessageReceiveService.processScheduleAssessedExerciseSubmittedNotification(exercise.getId()); - await().until(() -> notificationRepository.count() > sizeBefore); + await().until(() -> notificationTestRepository.count() > sizeBefore); verify(singleUserNotificationService, timeout(TIMEOUT_MS)).notifyUsersAboutAssessedExerciseSubmission(exercise); verify(javaMailSender, timeout(TIMEOUT_MS)).send(any(MimeMessage.class)); } diff --git a/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java index e30b73f91e5b..807d8d56ecbc 100644 --- a/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java @@ -22,6 +22,9 @@ import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.MESSAGE_REPLY_IN_CONVERSATION_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.NEW_PLAGIARISM_CASE_STUDENT_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.PLAGIARISM_CASE_VERDICT_STUDENT_TITLE; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.SSH_KEY_ADDED_TEXT; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.SSH_KEY_EXPIRES_SOON_TEXT; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.SSH_KEY_HAS_EXPIRED_TEXT; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_ASSIGNED_TEXT; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_ASSIGNED_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_DEREGISTRATION_STUDENT_TITLE; @@ -31,6 +34,9 @@ import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_REGISTRATION_TUTOR_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_UNASSIGNED_TEXT; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_UNASSIGNED_TITLE; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.VCS_ACCESS_TOKEN_ADDED_TEXT; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.VCS_ACCESS_TOKEN_EXPIRED_TEXT; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.VCS_ACCESS_TOKEN_EXPIRES_SOON_TEXT; import static de.tum.cit.aet.artemis.communication.service.notifications.NotificationSettingsService.NOTIFICATION_USER_NOTIFICATION_DATA_EXPORT_CREATED; import static de.tum.cit.aet.artemis.communication.service.notifications.NotificationSettingsService.NOTIFICATION_USER_NOTIFICATION_DATA_EXPORT_FAILED; import static de.tum.cit.aet.artemis.communication.service.notifications.NotificationSettingsService.NOTIFICATION__EXERCISE_NOTIFICATION__EXERCISE_SUBMISSION_ASSESSED; @@ -49,6 +55,7 @@ import static org.mockito.Mockito.verify; import java.io.IOException; +import java.security.GeneralSecurityException; import java.time.ZonedDateTime; import java.util.Comparator; import java.util.List; @@ -58,7 +65,10 @@ import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; +import org.apache.sshd.common.config.keys.AuthorizedKeyEntry; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -80,7 +90,6 @@ import de.tum.cit.aet.artemis.communication.domain.conversation.OneToOneChat; import de.tum.cit.aet.artemis.communication.domain.notification.Notification; import de.tum.cit.aet.artemis.communication.domain.notification.SingleUserNotification; -import de.tum.cit.aet.artemis.communication.repository.NotificationRepository; import de.tum.cit.aet.artemis.communication.repository.NotificationSettingRepository; import de.tum.cit.aet.artemis.communication.service.notifications.SingleUserNotificationService; import de.tum.cit.aet.artemis.core.domain.Course; @@ -88,6 +97,7 @@ import de.tum.cit.aet.artemis.core.domain.DomainObject; 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.test_repository.NotificationTestRepository; import de.tum.cit.aet.artemis.core.user.util.UserUtilService; import de.tum.cit.aet.artemis.core.util.CourseUtilService; import de.tum.cit.aet.artemis.exercise.domain.Exercise; @@ -101,6 +111,10 @@ import de.tum.cit.aet.artemis.plagiarism.domain.PlagiarismVerdict; import de.tum.cit.aet.artemis.plagiarism.domain.text.TextPlagiarismResult; import de.tum.cit.aet.artemis.plagiarism.domain.text.TextSubmissionElement; +import de.tum.cit.aet.artemis.programming.dto.UserSshPublicKeyDTO; +import de.tum.cit.aet.artemis.programming.service.sshuserkeys.UserSshPublicKeyExpiryNotificationService; +import de.tum.cit.aet.artemis.programming.service.sshuserkeys.UserSshPublicKeyService; +import de.tum.cit.aet.artemis.programming.service.tokens.UserTokenExpiryNotificationService; import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; import de.tum.cit.aet.artemis.text.domain.TextExercise; import de.tum.cit.aet.artemis.text.util.TextExerciseFactory; @@ -114,7 +128,7 @@ class SingleUserNotificationServiceTest extends AbstractSpringIntegrationIndepen private SingleUserNotificationService singleUserNotificationService; @Autowired - private NotificationRepository notificationRepository; + private NotificationTestRepository notificationTestRepository; @Autowired private NotificationSettingRepository notificationSettingRepository; @@ -134,6 +148,15 @@ class SingleUserNotificationServiceTest extends AbstractSpringIntegrationIndepen @Autowired private ParticipationUtilService participationUtilService; + @Autowired + private UserSshPublicKeyExpiryNotificationService userSshPublicKeyExpiryNotificationService; + + @Autowired + private UserTokenExpiryNotificationService userTokenExpiryNotificationService; + + @Autowired + private UserSshPublicKeyService userSshPublicKeyService; + @Captor private ArgumentCaptor appleNotificationCaptor; @@ -195,7 +218,7 @@ void setUp() { userTwo = userUtilService.getUserByLogin(TEST_PREFIX + "student2"); userThree = userUtilService.getUserByLogin(TEST_PREFIX + "student3"); - notificationRepository.deleteAllInBatch(); + notificationTestRepository.deleteAllInBatch(); exercise = new TextExercise(); exercise.setCourse(course); @@ -282,7 +305,7 @@ void setUp() { * @param expectedNotificationTitle is the title (NotificationTitleTypeConstants) of the expected notification */ private void verifyRepositoryCallWithCorrectNotification(String expectedNotificationTitle) { - List capturedNotifications = notificationRepository.findAll(); + List capturedNotifications = notificationTestRepository.findAll(); assertThat(capturedNotifications).isNotEmpty(); List relevantNotifications = capturedNotifications.stream().filter(e -> e.getTitle().equals(expectedNotificationTitle)).toList(); assertThat(relevantNotifications).as("Title of the captured notification should be equal to the expected one").hasSize(1); @@ -297,13 +320,13 @@ private void verifyRepositoryCallWithCorrectNotification(String expectedNotifica @Test void testSendNoNotificationOrEmailWhenSettingsAreDeactivated() { notificationSettingRepository.save(new NotificationSetting(user, false, true, true, NOTIFICATION__EXERCISE_NOTIFICATION__NEW_REPLY_FOR_EXERCISE_POST)); - assertThat(notificationRepository.findAll()).as("No notifications should be present prior to the method call").isEmpty(); + assertThat(notificationTestRepository.findAll()).as("No notifications should be present prior to the method call").isEmpty(); SingleUserNotification notification = singleUserNotificationService.createNotificationAboutNewMessageReply(answerPost, answerPost.getAuthor(), answerPost.getPost().getConversation()); singleUserNotificationService.notifyUserAboutNewMessageReply(answerPost, notification, user, userTwo, NEW_REPLY_FOR_EXERCISE_POST); - assertThat(notificationRepository.findAll()).as("The notification should have been saved to the DB").hasSize(1); + assertThat(notificationTestRepository.findAll()).as("The notification should have been saved to the DB").hasSize(1); // no web app notification or email should be sent verify(websocketMessagingService, never()).sendMessage(any(), any()); } @@ -342,7 +365,7 @@ void testNotifyUserAboutAssessedExerciseSubmission() { void testCheckNotificationForAssessmentExerciseSubmission_pastAssessmentDueDate() { exercise = TextExerciseFactory.generateTextExercise(null, null, ZonedDateTime.now().minusMinutes(1), course); singleUserNotificationService.checkNotificationForAssessmentExerciseSubmission(exercise, user, result); - assertThat(notificationRepository.findAll()).as("One new notification should have been created").hasSize(1); + assertThat(notificationTestRepository.findAll()).as("One new notification should have been created").hasSize(1); } /** @@ -352,7 +375,7 @@ void testCheckNotificationForAssessmentExerciseSubmission_pastAssessmentDueDate( void testCheckNotificationForAssessmentExerciseSubmission_futureAssessmentDueDate() { exercise = TextExerciseFactory.generateTextExercise(null, null, ZonedDateTime.now().plusHours(1), course); singleUserNotificationService.checkNotificationForAssessmentExerciseSubmission(exercise, user, result); - assertThat(notificationRepository.findAll()).as("No new notification should have been created").isEmpty(); + assertThat(notificationTestRepository.findAll()).as("No new notification should have been created").isEmpty(); } @Test @@ -375,13 +398,185 @@ void testNotifyUsersAboutAssessedExerciseSubmission() { singleUserNotificationService.notifyUsersAboutAssessedExerciseSubmission(testExercise); - List sentNotifications = notificationRepository.findAll(); + List sentNotifications = notificationTestRepository.findAll(); assertThat(sentNotifications).as("Only one notification should have been created (for the user with a valid participation, submission, and manual result)").hasSize(1); assertThat(sentNotifications.getFirst()).isInstanceOf(SingleUserNotification.class); assertThat(((SingleUserNotification) sentNotifications.getFirst()).getRecipient()).isEqualTo(studentWithParticipationAndSubmissionAndManualResult); } + // UserSshPublicKey related (expiry warning and newly created key) + + @Nested + class UserSshPublicKeyExpiryNotification { + + String RSA_KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEbgjoSpKnry5yuMiWh/uwhMG2Jq5Sh8Uw9vz+39or2i"; + + long KEY_ID = 4L; + + String KEY_LABEL = "key "; + + List sentNotifications; + + @AfterEach + void tearDown() { + userSshPublicKeyRepository.deleteAll(); + } + + @Test + void shouldNotifyUserAboutNewlyCreatedSshKeyWithExpirationDate() throws GeneralSecurityException, IOException { + UserSshPublicKeyDTO keyDTO = new UserSshPublicKeyDTO(KEY_ID, KEY_LABEL, RSA_KEY, null, null, null, null); + + userSshPublicKeyService.createSshKeyForUser(user, AuthorizedKeyEntry.parseAuthorizedKeyEntry(keyDTO.publicKey()), keyDTO); + + sentNotifications = notificationTestRepository.findAllByRecipientId(user.getId()); + checkFirstNotification(); + } + + @Test + void shouldNotifyUserAboutNewlyCreatedSshKeyWithNoDate() throws GeneralSecurityException, IOException { + UserSshPublicKeyDTO keyDTO = new UserSshPublicKeyDTO(KEY_ID, KEY_LABEL, RSA_KEY, null, null, null, ZonedDateTime.now().plusDays(15)); + + userSshPublicKeyService.createSshKeyForUser(user, AuthorizedKeyEntry.parseAuthorizedKeyEntry(keyDTO.publicKey()), keyDTO); + + sentNotifications = notificationTestRepository.findAllByRecipientId(user.getId()); + checkFirstNotification(); + } + + @Test + void shouldNotifyUserAboutUpcomingSshKeyExpiry() throws GeneralSecurityException, IOException { + UserSshPublicKeyDTO keyDTO = new UserSshPublicKeyDTO(KEY_ID, KEY_LABEL, RSA_KEY, null, null, null, ZonedDateTime.now().plusDays(6)); + userSshPublicKeyService.createSshKeyForUser(user, AuthorizedKeyEntry.parseAuthorizedKeyEntry(keyDTO.publicKey()), keyDTO); + + userSshPublicKeyExpiryNotificationService.notifyUserOnUpcomingKeyExpiry(); + + sentNotifications = notificationTestRepository.findAllByRecipientId(user.getId()); + assertThat(sentNotifications).hasSize(2); + assertThat(((SingleUserNotification) sentNotifications.getFirst()).getRecipient()).isEqualTo(user); + assertThat((sentNotifications.get(1)).getText()).isEqualTo(SSH_KEY_EXPIRES_SOON_TEXT); + checkFirstNotification(); + } + + @Test + void shouldNotifyUserAboutExpiredSshKey() throws GeneralSecurityException, IOException { + UserSshPublicKeyDTO keyDTO = new UserSshPublicKeyDTO(KEY_ID, KEY_LABEL, RSA_KEY, null, null, null, ZonedDateTime.now().minusDays(1)); + userSshPublicKeyService.createSshKeyForUser(user, AuthorizedKeyEntry.parseAuthorizedKeyEntry(keyDTO.publicKey()), keyDTO); + + userSshPublicKeyExpiryNotificationService.notifyUserOnExpiredKey(); + + sentNotifications = notificationTestRepository.findAllByRecipientId(user.getId()); + assertThat(sentNotifications).hasSize(2); + assertThat(((SingleUserNotification) sentNotifications.getFirst()).getRecipient()).isEqualTo(user); + assertThat((sentNotifications.get(1)).getText()).isEqualTo(SSH_KEY_HAS_EXPIRED_TEXT); + checkFirstNotification(); + } + + @Test + void shouldNotNotifyUserAboutUpcomingSshKeyExpiryWhenKeyDoesNotExpireSoon() throws GeneralSecurityException, IOException { + UserSshPublicKeyDTO keyDTO = new UserSshPublicKeyDTO(KEY_ID, KEY_LABEL, RSA_KEY, null, null, null, ZonedDateTime.now().plusDays(100)); + userSshPublicKeyService.createSshKeyForUser(user, AuthorizedKeyEntry.parseAuthorizedKeyEntry(keyDTO.publicKey()), keyDTO); + + userSshPublicKeyExpiryNotificationService.notifyUserOnUpcomingKeyExpiry(); + + sentNotifications = notificationTestRepository.findAllByRecipientId(user.getId()); + assertThat(sentNotifications).hasSize(1); + checkFirstNotification(); + } + + @Test + void shouldNotNotifyUserAboutExpiredSshKeyWhenKeyIsNotExpired() throws GeneralSecurityException, IOException { + UserSshPublicKeyDTO keyDTO = new UserSshPublicKeyDTO(KEY_ID, KEY_LABEL, RSA_KEY, null, null, null, ZonedDateTime.now().plusDays(100)); + userSshPublicKeyService.createSshKeyForUser(user, AuthorizedKeyEntry.parseAuthorizedKeyEntry(keyDTO.publicKey()), keyDTO); + + userSshPublicKeyExpiryNotificationService.notifyUserOnExpiredKey(); + + sentNotifications = notificationTestRepository.findAllByRecipientId(user.getId()); + assertThat(sentNotifications).hasSize(1); + checkFirstNotification(); + } + + @Test + void scheduleKeyExpiryNotifications() { + userSshPublicKeyExpiryNotificationService.sendKeyExpirationNotifications(); + + sentNotifications = notificationTestRepository.findAllByRecipientId(user.getId()); + assertThat(sentNotifications).hasSize(0); + } + + void checkFirstNotification() { + assertThat(sentNotifications.getFirst()).isInstanceOf(SingleUserNotification.class); + assertThat(((SingleUserNotification) sentNotifications.getFirst()).getRecipient()).isEqualTo(user); + assertThat((sentNotifications.getFirst()).getText()).isEqualTo(SSH_KEY_ADDED_TEXT); + } + } + + // User VCS access token related (expiry warning and newly added token) + + @Nested + class UserTokenExpiryNotification { + + List sentNotifications; + + @AfterEach + void tearDown() throws Exception { + user.setVcsAccessTokenExpiryDate(null); + user.setVcsAccessToken(null); + userTestRepository.save(user); + } + + @Test + void shouldNotifyUserAboutNewlyAddedVcsAccessToken() { + singleUserNotificationService.notifyUserAboutNewlyAddedVcsAccessToken(user); + + sentNotifications = notificationTestRepository.findAll(); + assertThat(sentNotifications.getFirst()).isInstanceOf(SingleUserNotification.class); + assertThat(((SingleUserNotification) sentNotifications.getFirst()).getRecipient()).isEqualTo(user); + assertThat((sentNotifications.getFirst()).getText()).isEqualTo(VCS_ACCESS_TOKEN_ADDED_TEXT); + } + + @Test + void shouldNotifyUserAboutSoonExpiringVcsAccessToken() { + user.setVcsAccessToken("token"); + user.setVcsAccessTokenExpiryDate(ZonedDateTime.now().minusHours(5).plusDays(7)); + userTestRepository.save(user); + + userTokenExpiryNotificationService.sendTokenExpirationNotifications(); + + sentNotifications = notificationTestRepository.findAll(); + assertThat(sentNotifications).hasSize(1); + assertThat(sentNotifications.getFirst()).isInstanceOf(SingleUserNotification.class); + assertThat(((SingleUserNotification) sentNotifications.getFirst()).getRecipient()).isEqualTo(user); + assertThat((sentNotifications.getFirst()).getText()).isEqualTo(VCS_ACCESS_TOKEN_EXPIRES_SOON_TEXT); + } + + @Test + void shouldNotifyUserAboutExpiredVcsAccessToken() { + user.setVcsAccessToken("token"); + user.setVcsAccessTokenExpiryDate(ZonedDateTime.now().minusHours(5)); + userTestRepository.save(user); + + userTokenExpiryNotificationService.sendTokenExpirationNotifications(); + + sentNotifications = notificationTestRepository.findAll(); + assertThat(sentNotifications).hasSize(1); + assertThat(sentNotifications.getFirst()).isInstanceOf(SingleUserNotification.class); + assertThat(((SingleUserNotification) sentNotifications.getFirst()).getRecipient()).isEqualTo(user); + assertThat((sentNotifications.getFirst()).getText()).isEqualTo(VCS_ACCESS_TOKEN_EXPIRED_TEXT); + } + + @Test + void shouldNotNotifyUserAboutVcsAccessTokenExpiryWhenTokenIsNotExpired() { + user.setVcsAccessToken("token"); + user.setVcsAccessTokenExpiryDate(ZonedDateTime.now().plusDays(5)); + userTestRepository.save(user); + + userTokenExpiryNotificationService.sendTokenExpirationNotifications(); + + sentNotifications = notificationTestRepository.findAll(); + assertThat(sentNotifications).hasSize(0); + } + } + // Plagiarism related /** @@ -421,9 +616,9 @@ void testNotifyUserAboutFinalPlagiarismState() throws MessagingException, IOExce @Test void testConversationNotificationsOneToOneChatCreation() { - var notificationsBefore = (int) notificationRepository.count(); + var notificationsBefore = (int) notificationTestRepository.count(); singleUserNotificationService.notifyClientAboutConversationCreationOrDeletion(oneToOneChat, user, userTwo, CONVERSATION_CREATE_ONE_TO_ONE_CHAT); - List capturedNotifications = notificationRepository.findAll(); + List capturedNotifications = notificationTestRepository.findAll(); assertThat(capturedNotifications).as("Notification should not have been saved").hasSize(notificationsBefore); // notification should be sent verify(websocketMessagingService).sendMessage(eq("/topic/user/" + user.getId() + "/notifications"), (Object) any()); @@ -431,14 +626,14 @@ void testConversationNotificationsOneToOneChatCreation() { @Test void testConversationNotificationsGroupChatCreation() { - int notificationsBefore = (int) notificationRepository.count(); + int notificationsBefore = (int) notificationTestRepository.count(); singleUserNotificationService.notifyClientAboutConversationCreationOrDeletion(groupChat, user, userTwo, CONVERSATION_CREATE_GROUP_CHAT); verify(websocketMessagingService).sendMessage(eq("/topic/user/" + user.getId() + "/notifications"), (Object) any()); singleUserNotificationService.notifyClientAboutConversationCreationOrDeletion(groupChat, userThree, userTwo, CONVERSATION_CREATE_GROUP_CHAT); verify(websocketMessagingService).sendMessage(eq("/topic/user/" + userThree.getId() + "/notifications"), (Object) any()); - List capturedNotifications = notificationRepository.findAll(); + List capturedNotifications = notificationTestRepository.findAll(); assertThat(capturedNotifications).as("Both notifications should have been saved").hasSize(notificationsBefore + 2); capturedNotifications.forEach(capturedNotification -> { assertThat(capturedNotification.getTitle()).as("Title of the captured notification should be equal to the expected one") @@ -482,7 +677,7 @@ void testConversationNotificationsNewMessageReply() { answerPost.getPost().getConversation()); singleUserNotificationService.notifyUserAboutNewMessageReply(answerPost, notification, user, userTwo, CONVERSATION_NEW_REPLY_MESSAGE); verify(websocketMessagingService, never()).sendMessage(eq("/topic/user/" + user.getId() + "/notifications"), (Object) any()); - Notification sentNotification = notificationRepository.findAll().stream().max(Comparator.comparing(DomainObject::getId)).orElseThrow(); + Notification sentNotification = notificationTestRepository.findAll().stream().max(Comparator.comparing(DomainObject::getId)).orElseThrow(); SingleUserNotificationService.NewReplyNotificationSubject notificationSubject = new SingleUserNotificationService.NewReplyNotificationSubject(answerPost, user, userTwo); verify(generalInstantNotificationService, times(1)).sendNotification(sentNotification, user, notificationSubject); @@ -581,7 +776,7 @@ void testDataExportNotification_dataExportFailed() { } /** - * Checks if an email was created and send + * Checks if an email was created and sent */ private void verifyEmail() { verify(javaMailSender, timeout(1000)).send(any(MimeMessage.class)); diff --git a/src/test/java/de/tum/cit/aet/artemis/communication/notifications/service/ConversationNotificationServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/communication/notifications/service/ConversationNotificationServiceTest.java index 11911a26af6e..cf646f594ef0 100644 --- a/src/test/java/de/tum/cit/aet/artemis/communication/notifications/service/ConversationNotificationServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/communication/notifications/service/ConversationNotificationServiceTest.java @@ -20,7 +20,6 @@ import de.tum.cit.aet.artemis.communication.domain.notification.ConversationNotification; import de.tum.cit.aet.artemis.communication.domain.notification.Notification; import de.tum.cit.aet.artemis.communication.repository.ConversationMessageRepository; -import de.tum.cit.aet.artemis.communication.repository.NotificationRepository; import de.tum.cit.aet.artemis.communication.repository.conversation.ConversationNotificationRepository; import de.tum.cit.aet.artemis.communication.service.notifications.ConversationNotificationService; import de.tum.cit.aet.artemis.communication.test_repository.ConversationParticipantTestRepository; @@ -28,6 +27,7 @@ import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.domain.DomainObject; import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.core.test_repository.NotificationTestRepository; import de.tum.cit.aet.artemis.core.test_repository.UserTestRepository; import de.tum.cit.aet.artemis.core.user.util.UserUtilService; import de.tum.cit.aet.artemis.core.util.CourseUtilService; @@ -62,7 +62,7 @@ class ConversationNotificationServiceTest extends AbstractSpringIntegrationIndep private CourseUtilService courseUtilService; @Autowired - private NotificationRepository notificationRepository; + private NotificationTestRepository notificationTestRepository; private OneToOneChat oneToOneChat; @@ -115,7 +115,7 @@ void createNotificationForNewMessageInConversation() { conversationNotificationService.notifyAboutNewMessage(post, notification, Set.of(user2)); verifyRepositoryCallWithCorrectNotification(NEW_MESSAGE_TITLE); - Notification sentNotification = notificationRepository.findAll().stream().max(Comparator.comparing(DomainObject::getId)).orElseThrow(); + Notification sentNotification = notificationTestRepository.findAll().stream().max(Comparator.comparing(DomainObject::getId)).orElseThrow(); verify(generalInstantNotificationService).sendNotification(sentNotification, Set.of(user2), post); diff --git a/src/test/java/de/tum/cit/aet/artemis/core/test_repository/NotificationTestRepository.java b/src/test/java/de/tum/cit/aet/artemis/core/test_repository/NotificationTestRepository.java new file mode 100644 index 000000000000..5ac06e0cc6c9 --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/core/test_repository/NotificationTestRepository.java @@ -0,0 +1,23 @@ +package de.tum.cit.aet.artemis.core.test_repository; + +import java.util.List; + +import org.springframework.context.annotation.Primary; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import de.tum.cit.aet.artemis.communication.domain.notification.Notification; +import de.tum.cit.aet.artemis.communication.repository.NotificationRepository; + +@Repository +@Primary +public interface NotificationTestRepository extends NotificationRepository { + + @Query(""" + SELECT notification + FROM Notification notification + LEFT JOIN TREAT(notification AS SingleUserNotification).recipient recipient + WHERE recipient.id = :recipientId + """) + List findAllByRecipientId(long recipientId); +} 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 758be1318ad9..ba14d53750b8 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 @@ -42,6 +42,7 @@ import de.tum.cit.aet.artemis.core.security.Role; import de.tum.cit.aet.artemis.core.service.user.PasswordService; import de.tum.cit.aet.artemis.core.test_repository.CourseTestRepository; +import de.tum.cit.aet.artemis.core.test_repository.NotificationTestRepository; 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; @@ -125,6 +126,9 @@ public class UserTestService { @Autowired private ExerciseTestRepository exerciseTestRepository; + @Autowired + private NotificationTestRepository notificationTestRepository; + private String TEST_PREFIX; private MockDelegate mockDelegate; @@ -160,6 +164,9 @@ public void setup(String testPrefix, MockDelegate mockDelegate) throws Exception } public void tearDown() throws IOException { + if (student.getId() != null) { + notificationTestRepository.deleteAllInBatch(notificationTestRepository.findAllByRecipientId(student.getId())); + } userTestRepository.deleteAll(userTestRepository.searchAllByLoginOrName(Pageable.unpaged(), TEST_PREFIX)); } diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCSshSettingsTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCSshSettingsTest.java index fffe4a6ec0b3..bdcb2a2741b8 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCSshSettingsTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCSshSettingsTest.java @@ -1,8 +1,12 @@ package de.tum.cit.aet.artemis.programming.icl; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; @@ -18,11 +22,13 @@ class LocalVCSshSettingsTest extends AbstractSpringIntegrationLocalCILocalVCTest @BeforeEach void setUp() throws Exception { + doNothing().when(singleUserNotificationService).notifyUserAboutNewlyAddedSshKey(any(), any()); sshSettingsTestService.setup(TEST_PREFIX); } @AfterEach void teardown() throws Exception { + Mockito.reset(singleUserNotificationService); sshSettingsTestService.tearDown(TEST_PREFIX); } diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/util/SshSettingsTestService.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/util/SshSettingsTestService.java index d2a6be51dcbe..4f7bebb55025 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/util/SshSettingsTestService.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/util/SshSettingsTestService.java @@ -123,14 +123,9 @@ public void failToAddOrDeleteWithInvalidKeyId() throws Exception { var validKey = createNewValidSSHKey(user, sshKey1); request.postWithResponseBody(requestPrefix + "public-key", validKey, String.class, HttpStatus.OK); - var userKey = userSshPublicKeyRepository.findAll().getFirst(); - userKey.setUserId(12L); - userSshPublicKeyRepository.save(userKey); request.delete(requestPrefix + "public-key/3443", HttpStatus.FORBIDDEN); request.get(requestPrefix + "public-key/43443", HttpStatus.FORBIDDEN, UserSshPublicKeyDTO.class); - request.get(requestPrefix + "public-key/" + userKey.getId(), HttpStatus.FORBIDDEN, UserSshPublicKeyDTO.class); - } // Test From 1eb018d3b1f2708b8fc0266b43742f0762430cd7 Mon Sep 17 00:00:00 2001 From: Martin Felber <45291671+FelberMartin@users.noreply.github.com> Date: Tue, 25 Feb 2025 19:03:52 +0100 Subject: [PATCH 08/17] General: Switch course title text color based on course color (#10375) --- .../course-management-card.component.html | 2 +- .../course-management-card.component.ts | 3 ++ .../overview/course-management-card.scss | 3 +- .../app/lti/lti-course-card.component.html | 2 +- .../app/lti/lti-course-card.component.ts | 3 ++ .../course-card-header.component.html | 2 +- .../course-card-header.component.ts | 3 ++ .../app/overview/header-course.component.html | 6 ++-- .../app/overview/header-course.component.ts | 5 +++ src/main/webapp/app/utils/color.utils.ts | 36 +++++++++++++++++++ 10 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/main/webapp/app/course/manage/overview/course-management-card.component.html b/src/main/webapp/app/course/manage/overview/course-management-card.component.html index 688a3000196a..2c80b6fd5b47 100644 --- a/src/main/webapp/app/course/manage/overview/course-management-card.component.html +++ b/src/main/webapp/app/course/manage/overview/course-management-card.component.html @@ -1,5 +1,5 @@
-
+
@if (course.courseIcon) { diff --git a/src/main/webapp/app/course/manage/overview/course-management-card.component.ts b/src/main/webapp/app/course/manage/overview/course-management-card.component.ts index f4eb8a31a545..3f0f5c88d78b 100644 --- a/src/main/webapp/app/course/manage/overview/course-management-card.component.ts +++ b/src/main/webapp/app/course/manage/overview/course-management-card.component.ts @@ -38,6 +38,7 @@ import { FeatureToggleLinkDirective } from 'app/shared/feature-toggle/feature-to import { FeatureToggleHideDirective } from 'app/shared/feature-toggle/feature-toggle-hide.directive'; import { ArtemisDatePipe } from 'app/shared/pipes/artemis-date.pipe'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { getContrastingTextColor } from 'app/utils/color.utils'; @Component({ selector: 'jhi-course-management-card', @@ -112,6 +113,7 @@ export class CourseManagementCardComponent implements OnInit, OnChanges { faQuestion = faQuestion; courseColor: string; + contentColor: string; readonly FeatureToggle = FeatureToggle; @@ -126,6 +128,7 @@ export class CourseManagementCardComponent implements OnInit, OnChanges { const targetCourseColor = this.course.color || this.ARTEMIS_DEFAULT_COLOR; if (this.courseColor !== targetCourseColor) { this.courseColor = targetCourseColor; + this.contentColor = getContrastingTextColor(this.courseColor); } // Only sort one time once loaded diff --git a/src/main/webapp/app/course/manage/overview/course-management-card.scss b/src/main/webapp/app/course/manage/overview/course-management-card.scss index d48a2845f0e7..d806f325688a 100644 --- a/src/main/webapp/app/course/manage/overview/course-management-card.scss +++ b/src/main/webapp/app/course/manage/overview/course-management-card.scss @@ -16,6 +16,7 @@ filter: alpha(opacity = 100); transition: 0.15s; background-color: var(--background-color-for-hover) !important; + color: var(--content-color); display: flex; cursor: pointer; @@ -77,7 +78,7 @@ padding-right: 7px; a { - color: white; + color: var(--content-color); } } } diff --git a/src/main/webapp/app/lti/lti-course-card.component.html b/src/main/webapp/app/lti/lti-course-card.component.html index 9db18ceaca4b..b8c4fa6667f7 100644 --- a/src/main/webapp/app/lti/lti-course-card.component.html +++ b/src/main/webapp/app/lti/lti-course-card.component.html @@ -3,7 +3,7 @@ id="course-{{ course().id }}-header" class="card-header text-white" [routerLink]="['/lti/exercises', course().id]" - [ngStyle]="{ '--background-color-for-hover': courseColor }" + [ngStyle]="{ '--background-color-for-hover': courseColor, '--content-color': contentColor }" >
diff --git a/src/main/webapp/app/lti/lti-course-card.component.ts b/src/main/webapp/app/lti/lti-course-card.component.ts index 147f11111224..bff596f390bb 100644 --- a/src/main/webapp/app/lti/lti-course-card.component.ts +++ b/src/main/webapp/app/lti/lti-course-card.component.ts @@ -4,6 +4,7 @@ import { ARTEMIS_DEFAULT_COLOR } from 'app/app.constants'; import { CachingStrategy, SecuredImageComponent } from 'app/shared/image/secured-image.component'; import { RouterLink } from '@angular/router'; import { NgStyle } from '@angular/common'; +import { getContrastingTextColor } from 'app/utils/color.utils'; @Component({ selector: 'jhi-overview-lti-course-card', @@ -16,11 +17,13 @@ export class LtiCourseCardComponent { course = input.required(); CachingStrategy = CachingStrategy; courseColor: string; + contentColor: string; constructor() { effect(() => { const courseValue = this.course(); this.courseColor = courseValue?.color || this.ARTEMIS_DEFAULT_COLOR; + this.contentColor = getContrastingTextColor(this.courseColor); }); } } diff --git a/src/main/webapp/app/overview/course-card-header/course-card-header.component.html b/src/main/webapp/app/overview/course-card-header/course-card-header.component.html index 69b6b496fa1e..37000e6ce148 100644 --- a/src/main/webapp/app/overview/course-card-header/course-card-header.component.html +++ b/src/main/webapp/app/overview/course-card-header/course-card-header.component.html @@ -16,7 +16,7 @@
}
-
+
{{ courseTitle() }}
diff --git a/src/main/webapp/app/overview/course-card-header/course-card-header.component.ts b/src/main/webapp/app/overview/course-card-header/course-card-header.component.ts index b50cfccf45a1..7a06fb80ca4b 100644 --- a/src/main/webapp/app/overview/course-card-header/course-card-header.component.ts +++ b/src/main/webapp/app/overview/course-card-header/course-card-header.component.ts @@ -3,6 +3,7 @@ import { CachingStrategy, SecuredImageComponent } from 'app/shared/image/secured import { ARTEMIS_DEFAULT_COLOR } from 'app/app.constants'; import { RouterModule } from '@angular/router'; import { CommonModule, SlicePipe } from '@angular/common'; +import { getContrastingTextColor } from 'app/utils/color.utils'; @Component({ selector: 'jhi-course-card-header', @@ -20,8 +21,10 @@ export class CourseCardHeaderComponent implements OnInit { CachingStrategy = CachingStrategy; color: string; + titleColor: string; ngOnInit() { this.color = this.courseColor() || this.ARTEMIS_DEFAULT_COLOR; + this.titleColor = getContrastingTextColor(this.color); } } diff --git a/src/main/webapp/app/overview/header-course.component.html b/src/main/webapp/app/overview/header-course.component.html index 43a30615e2c8..eb11ae2a2c36 100644 --- a/src/main/webapp/app/overview/header-course.component.html +++ b/src/main/webapp/app/overview/header-course.component.html @@ -1,12 +1,12 @@ -
+
-

{{ course.title }}

+

{{ course.title }}

@if (courseDescription) {
-
{{ courseDescription }}
+
{{ courseDescription }}
@if (enableShowMore) {
{{ 'artemisApp.courseOverview.' + (longDescriptionShown ? 'showLess' : 'showMore') | artemisTranslate }} diff --git a/src/main/webapp/app/overview/header-course.component.ts b/src/main/webapp/app/overview/header-course.component.ts index d698b4d429ce..8d814440f50d 100644 --- a/src/main/webapp/app/overview/header-course.component.ts +++ b/src/main/webapp/app/overview/header-course.component.ts @@ -9,6 +9,7 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { TranslateDirective } from '../shared/language/translate.directive'; import { SecuredImageComponent } from '../shared/image/secured-image.component'; import { ArtemisTranslatePipe } from '../shared/pipes/artemis-translate.pipe'; +import { getContrastingTextColor } from 'app/utils/color.utils'; @Component({ selector: 'jhi-header-course', @@ -24,6 +25,8 @@ export class HeaderCourseComponent implements OnChanges { @Input() public course: Course; + public courseColor: string; + public contentColor: string; public courseDescription?: string; public enableShowMore = false; public longDescriptionShown = false; @@ -31,6 +34,8 @@ export class HeaderCourseComponent implements OnChanges { faArrowDown = faArrowDown; ngOnChanges() { + this.courseColor = this.course.color || ARTEMIS_DEFAULT_COLOR; + this.contentColor = getContrastingTextColor(this.courseColor); this.adjustCourseDescription(); } diff --git a/src/main/webapp/app/utils/color.utils.ts b/src/main/webapp/app/utils/color.utils.ts index 022f83b0dbdf..9aa9c863ad4b 100644 --- a/src/main/webapp/app/utils/color.utils.ts +++ b/src/main/webapp/app/utils/color.utils.ts @@ -11,3 +11,39 @@ export const getBackgroundColorHue = (seed: string | undefined): string => { const hue = deterministicRandomValueFromString(seed) * 360; return `hsl(${hue}, 50%, 50%)`; // Return an HSL color string }; + +/** + * Returns the brightness of a color. The calculation is based on https://www.w3.org/TR/AERT/#color-contrast + * @param {string} color - The color in hex format. + * @returns {number} - The brightness of the color. + */ +export const getColorBrightness = (color: string): number => { + // Remove the hash at the start if it's there + color = color.replace('#', ''); + + // Parse the r, g, b values + const r = parseInt(color.substring(0, 2), 16); + const g = parseInt(color.substring(2, 4), 16); + const b = parseInt(color.substring(4, 6), 16); + + // Calculate the brightness + return (r * 299 + g * 587 + b * 114) / 1000; +}; + +/** + * Determines if a color is dark based on its brightness. + * @param {string} color - The color in hex format. + * @returns {boolean} - True if the color is dark, otherwise false. + */ +export const isColorDark = (color: string): boolean => { + return getColorBrightness(color) < 128; +}; + +/** + * Returns either black or white depending on the background color brightness. + * @param {string} color - The background color in hex format. + * @returns {string} - 'black' if the background color is light, 'white' if the background color is dark. + */ +export const getContrastingTextColor = (color: string): string => { + return isColorDark(color) ? 'white' : 'black'; +}; From d682405700e3be1c60199d5b1b302453e049a659 Mon Sep 17 00:00:00 2001 From: Simon Entholzer <33342534+SimonEntholzer@users.noreply.github.com> Date: Tue, 25 Feb 2025 19:04:58 +0100 Subject: [PATCH 09/17] Programming exercises: Disable illegal submission notifications for intsructors submitting after the exercise due date (#10380) --- .../service/ProgrammingSubmissionService.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) 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 e1ae83935eec..b6d8375082a7 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 @@ -269,11 +269,15 @@ private void checkForIllegalSubmission(ProgrammingExerciseParticipation programm if (optionalStudentWithGroups.isEmpty()) { return; } - User student = optionalStudentWithGroups.get(); + User user = optionalStudentWithGroups.get(); - if (!isAllowedToSubmit(studentParticipation, student, programmingSubmission)) { + if (authCheckService.isAtLeastInstructorForExercise(studentParticipation.getExercise(), user)) { + return; + } + + if (!isAllowedToSubmit(studentParticipation, user, programmingSubmission)) { final String message = ("The student %s illegally submitted code after the allowed individual due date (including the grace period) in the participation %d for the " - + "programming exercise \"%s\"").formatted(student.getLogin(), programmingExerciseParticipation.getId(), programmingExercise.getTitle()); + + "programming exercise \"%s\"").formatted(user.getLogin(), programmingExerciseParticipation.getId(), programmingExercise.getTitle()); programmingSubmission.setType(SubmissionType.ILLEGAL); programmingMessagingService.notifyInstructorGroupAboutIllegalSubmissionsForExercise(programmingExercise, message); log.warn(message); @@ -283,7 +287,7 @@ private void checkForIllegalSubmission(ProgrammingExerciseParticipation programm // we include submission policies here: if the student (for whatever reason) has more submission than allowed attempts, the submission would be illegal if (exceedsSubmissionPolicy(studentParticipation, submissionPolicy)) { final String message = "The student %s illegally submitted code after the submission policy lock limit %d in the participation %d for the programming exercise \"%s\"" - .formatted(student.getLogin(), submissionPolicy.getSubmissionLimit(), programmingExerciseParticipation.getId(), programmingExercise.getTitle()); + .formatted(user.getLogin(), submissionPolicy.getSubmissionLimit(), programmingExerciseParticipation.getId(), programmingExercise.getTitle()); programmingSubmission.setType(SubmissionType.ILLEGAL); programmingMessagingService.notifyInstructorGroupAboutIllegalSubmissionsForExercise(programmingExercise, message); log.warn(message); From 677c0700839e6c5de877b47b6e45e6249d9c93bf Mon Sep 17 00:00:00 2001 From: Martin Felber <45291671+FelberMartin@users.noreply.github.com> Date: Tue, 25 Feb 2025 19:11:31 +0100 Subject: [PATCH 10/17] Communication: Fix group chat notifications with null as name (#10381) --- .../domain/conversation/GroupChat.java | 6 ++- .../ConversationNotificationServiceTest.java | 41 +++++++++++++++---- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/domain/conversation/GroupChat.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/conversation/GroupChat.java index 32cbf12b3726..bd7745cdbdc5 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/domain/conversation/GroupChat.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/conversation/GroupChat.java @@ -75,7 +75,11 @@ public void setName(String name) { @Override public String getHumanReadableNameForReceiver(User sender) { - return getName(); + String name = getName(); + if (name == null || name.isBlank()) { + return generateName(); + } + return name; } @Override diff --git a/src/test/java/de/tum/cit/aet/artemis/communication/notifications/service/ConversationNotificationServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/communication/notifications/service/ConversationNotificationServiceTest.java index cf646f594ef0..50c98441880c 100644 --- a/src/test/java/de/tum/cit/aet/artemis/communication/notifications/service/ConversationNotificationServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/communication/notifications/service/ConversationNotificationServiceTest.java @@ -16,6 +16,8 @@ import de.tum.cit.aet.artemis.communication.domain.ConversationParticipant; import de.tum.cit.aet.artemis.communication.domain.Post; +import de.tum.cit.aet.artemis.communication.domain.conversation.Conversation; +import de.tum.cit.aet.artemis.communication.domain.conversation.GroupChat; 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; @@ -66,6 +68,8 @@ class ConversationNotificationServiceTest extends AbstractSpringIntegrationIndep private OneToOneChat oneToOneChat; + private GroupChat groupChat; + private User user1; private User user2; @@ -91,6 +95,13 @@ void setUp() { oneToOneChat.setConversationParticipants(Set.of(conversationParticipant1, conversationParticipant2)); oneToOneChat = conversationRepository.save(oneToOneChat); + groupChat = new GroupChat(); + groupChat.setCourse(course); + groupChat.setCreator(user1); + groupChat.setCreationDate(ZonedDateTime.now()); + groupChat.setConversationParticipants(Set.of(conversationParticipant1, conversationParticipant2)); + groupChat = conversationRepository.save(groupChat); + conversationNotificationRepository.deleteAll(); } @@ -103,13 +114,7 @@ private void verifyRepositoryCallWithCorrectNotification(String expectedNotifica @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void createNotificationForNewMessageInConversation() { - Post post = new Post(); - post.setAuthor(user1); - post.setCreationDate(ZonedDateTime.now()); - post.setConversation(oneToOneChat); - post.setVisibleForStudents(true); - post.setContent("hi test"); - post = conversationMessageRepository.save(post); + Post post = createAndSavePostForUser(user1, oneToOneChat); ConversationNotification notification = conversationNotificationService.createNotification(post, oneToOneChat, course, Set.of()); conversationNotificationService.notifyAboutNewMessage(post, notification, Set.of(user2)); @@ -125,4 +130,26 @@ void createNotificationForNewMessageInConversation() { conversationParticipantRepository.deleteAllById(participants.stream().map(ConversationParticipant::getId).toList()); conversationRepository.deleteAllById(List.of(oneToOneChat.getId())); } + + // This caused a bug in notifications on the mobile apps, see https://github.com/ls1intum/artemis-android/issues/391 + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void createNotificationForNewMessageInGroupChatContainsNonNullGroupName() { + Post post = createAndSavePostForUser(user1, groupChat); + + ConversationNotification notification = conversationNotificationService.createNotification(post, groupChat, course, Set.of()); + String[] notificationPlaceholders = notification.getTransientPlaceholderValuesAsArray(); + String conversationName = notificationPlaceholders[3]; + assertThat(conversationName).isNotNull(); + } + + private Post createAndSavePostForUser(User user, Conversation conversation) { + Post post = new Post(); + post.setAuthor(user); + post.setCreationDate(ZonedDateTime.now()); + post.setConversation(conversation); + post.setVisibleForStudents(true); + post.setContent("hi test"); + return conversationMessageRepository.save(post); + } } From 01d225d85b3afbd9056d4a1d87e712f96e95a9e8 Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Tue, 25 Feb 2025 08:33:49 +0100 Subject: [PATCH 11/17] Development: Cleanup server code --- .../artemis/assessment/service/ResultService.java | 2 +- .../cit/aet/artemis/core/config/WebConfigurer.java | 1 - .../artemis/core/exception/ArtemisMailException.java | 12 ------------ .../service/export/DataExportCreationService.java | 3 +-- .../core/web/admin/AdminBuildJobQueueResource.java | 5 +---- .../service/ContinuousPlagiarismControlService.java | 3 +-- .../artemis/programming/dto/ConsistencyErrorDTO.java | 2 +- ...ProgrammingExerciseCodeReviewFeedbackService.java | 1 - .../localci/SharedQueueManagementService.java | 7 +++---- .../service/TutorialGroupScheduleService.java | 7 ++++--- .../ForwardedMessageResourceIntegrationTest.java | 3 +-- .../core/connector/AeolusRequestMockProvider.java | 2 +- .../participation/ParticipationIntegrationTest.java | 1 - .../cit/aet/artemis/programming/GitServiceTest.java | 4 ++-- .../aet/artemis/programming/util/GitUtilService.java | 2 +- .../shared/architecture/ArchitectureTest.java | 1 - .../AbstractModuleResourceArchitectureTest.java | 1 - 17 files changed, 17 insertions(+), 40 deletions(-) delete mode 100644 src/main/java/de/tum/cit/aet/artemis/core/exception/ArtemisMailException.java diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/service/ResultService.java b/src/main/java/de/tum/cit/aet/artemis/assessment/service/ResultService.java index 17ccb2ca9fa6..41a8c0261461 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/service/ResultService.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/service/ResultService.java @@ -629,7 +629,7 @@ public FeedbackAnalysisResponseDTO getFeedbackDetailsOnPage(long exerciseId, Fee final Page feedbackDetailPage = studentParticipationRepository.findFilteredFeedbackByExerciseId(exerciseId, StringUtils.isBlank(data.getSearchTerm()) ? "" : data.getSearchTerm().toLowerCase(), data.getFilterTestCases(), includeNotAssignedToTask, minOccurrence, maxOccurrence, filterErrorCategories, pageable); - ; + List processedDetails; int totalPages = 0; long totalCount = 0; diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/WebConfigurer.java b/src/main/java/de/tum/cit/aet/artemis/core/config/WebConfigurer.java index 031e4a65930f..104dcfa7aad6 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/config/WebConfigurer.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/WebConfigurer.java @@ -136,6 +136,5 @@ public CorsFilter corsFilter() { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(toolsInterceptor).addPathPatterns("/api/**").excludePathPatterns("/api/public/**"); - ; } } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/exception/ArtemisMailException.java b/src/main/java/de/tum/cit/aet/artemis/core/exception/ArtemisMailException.java deleted file mode 100644 index f0edf3676b29..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/core/exception/ArtemisMailException.java +++ /dev/null @@ -1,12 +0,0 @@ -package de.tum.cit.aet.artemis.core.exception; - -public class ArtemisMailException extends RuntimeException { - - public ArtemisMailException(String message) { - super(message); - } - - public ArtemisMailException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/export/DataExportCreationService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/export/DataExportCreationService.java index 0b0061254975..cfdb2e266be6 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/export/DataExportCreationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/export/DataExportCreationService.java @@ -24,7 +24,6 @@ import de.tum.cit.aet.artemis.core.domain.DataExport; import de.tum.cit.aet.artemis.core.domain.DataExportState; import de.tum.cit.aet.artemis.core.domain.User; -import de.tum.cit.aet.artemis.core.exception.ArtemisMailException; import de.tum.cit.aet.artemis.core.repository.DataExportRepository; import de.tum.cit.aet.artemis.core.service.FileService; import de.tum.cit.aet.artemis.core.service.ResourceLoaderService; @@ -147,7 +146,7 @@ public boolean createDataExport(DataExport dataExport) { try { singleUserNotificationService.notifyUserAboutDataExportCreation(dataExport); } - catch (ArtemisMailException e) { + catch (Exception e) { log.warn("Failed to send email about successful data export creation"); } return true; diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminBuildJobQueueResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminBuildJobQueueResource.java index f1b04ec0bd24..70888ac32eb6 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminBuildJobQueueResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminBuildJobQueueResource.java @@ -98,10 +98,7 @@ public ResponseEntity getBuildAgentDetails(@RequestParam log.debug("REST request to get information on build agent {}", agentName); Optional buildAgentDetails = localCIBuildJobQueueService.getBuildAgentInformation().stream() .filter(agent -> agent.buildAgent().name().equals(agentName)).findFirst(); - if (buildAgentDetails.isEmpty()) { - return ResponseEntity.notFound().build(); - } - return ResponseEntity.ok(buildAgentDetails.get()); + return buildAgentDetails.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/plagiarism/service/ContinuousPlagiarismControlService.java b/src/main/java/de/tum/cit/aet/artemis/plagiarism/service/ContinuousPlagiarismControlService.java index 3c2933133a9b..bc58909e8a62 100644 --- a/src/main/java/de/tum/cit/aet/artemis/plagiarism/service/ContinuousPlagiarismControlService.java +++ b/src/main/java/de/tum/cit/aet/artemis/plagiarism/service/ContinuousPlagiarismControlService.java @@ -15,7 +15,6 @@ import de.jplag.exceptions.ExitException; import de.tum.cit.aet.artemis.communication.domain.DisplayPriority; import de.tum.cit.aet.artemis.communication.domain.Post; -import de.tum.cit.aet.artemis.core.exception.ArtemisMailException; import de.tum.cit.aet.artemis.core.util.TimeLogUtil; import de.tum.cit.aet.artemis.exercise.domain.Exercise; import de.tum.cit.aet.artemis.exercise.repository.ExerciseRepository; @@ -152,7 +151,7 @@ private void createOrUpdatePlagiarismCases(PlagiarismComparison comparison) { try { plagiarismPostService.createContinuousPlagiarismControlPlagiarismCasePost(post); } - catch (ArtemisMailException e) { + catch (Exception e) { // Catch mail exceptions to so that notification for the second student will be delivered log.error("Cannot send a cpc email: postId={}, plagiarismCaseId={}.", post.getId(), post.getPlagiarismCase().getId()); } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/dto/ConsistencyErrorDTO.java b/src/main/java/de/tum/cit/aet/artemis/programming/dto/ConsistencyErrorDTO.java index 4e4e582cbebe..a7a3b07d46c4 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/dto/ConsistencyErrorDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/dto/ConsistencyErrorDTO.java @@ -12,6 +12,6 @@ public record ConsistencyErrorDTO(ProgrammingExercise programmingExercise, ErrorType type) { public enum ErrorType { - VCS_PROJECT_MISSING, TEMPLATE_REPO_MISSING, SOLUTION_REPO_MISSING, AUXILIARY_REPO_MISSING, TEST_REPO_MISSING, TEMPLATE_BUILD_PLAN_MISSING, SOLUTION_BUILD_PLAN_MISSING; + VCS_PROJECT_MISSING, TEMPLATE_REPO_MISSING, SOLUTION_REPO_MISSING, AUXILIARY_REPO_MISSING, TEST_REPO_MISSING, TEMPLATE_BUILD_PLAN_MISSING, SOLUTION_BUILD_PLAN_MISSING } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java index 7f9942cc4dba..49af6f709a60 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java @@ -160,7 +160,6 @@ public void generateAutomaticNonGradedFeedback(ProgrammingExerciseStudentPartici feedback.setCredits(individualFeedbackItem.credits()); return feedback; }).sorted(Comparator.comparing(Feedback::getCredits, Comparator.nullsLast(Comparator.naturalOrder()))).toList(); - ; automaticResult.setSuccessful(true); automaticResult.setCompletionDate(ZonedDateTime.now()); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/SharedQueueManagementService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/SharedQueueManagementService.java index 621beac482e8..aac44a90867f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/SharedQueueManagementService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/SharedQueueManagementService.java @@ -482,10 +482,9 @@ private void updateBuildAgentCapacity() { public BuildTimingInfo isSubmissionProcessing(long participationId, String commitHash) { var buildJob = getProcessingJobs().stream().filter(job -> job.participationId() == participationId && Objects.equals(commitHash, job.buildConfig().assignmentCommitHash())) .findFirst(); - if (buildJob.isPresent()) { - return new BuildTimingInfo(buildJob.get().jobTimingInfo().buildStartDate(), buildJob.get().jobTimingInfo().estimatedCompletionDate()); - } - return null; + return buildJob + .map(buildJobQueueItem -> new BuildTimingInfo(buildJobQueueItem.jobTimingInfo().buildStartDate(), buildJobQueueItem.jobTimingInfo().estimatedCompletionDate())) + .orElse(null); } public record BuildTimingInfo(ZonedDateTime buildStartDate, ZonedDateTime estimatedCompletionDate) { diff --git a/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/service/TutorialGroupScheduleService.java b/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/service/TutorialGroupScheduleService.java index 5ebf0e8f5ca3..fceeac4c4936 100644 --- a/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/service/TutorialGroupScheduleService.java +++ b/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/service/TutorialGroupScheduleService.java @@ -186,11 +186,12 @@ public void updateScheduleIfChanged(TutorialGroupsConfiguration tutorialGroupsCo } } } - else if (oldSchedule.isPresent()) { // old schedule present but not new schedule -> delete old schedule + else // new schedule present but not old schedule -> create new schedule + if (oldSchedule.isPresent()) { // old schedule present but not new schedule -> delete old schedule tutorialGroupScheduleRepository.delete(oldSchedule.get()); } - else if (newSchedule.isPresent()) { // new schedule present but not old schedule -> create new schedule - saveScheduleAndGenerateScheduledSessions(tutorialGroupsConfiguration, tutorialGroup, newSchedule.get()); + else { + newSchedule.ifPresent(tutorialGroupSchedule -> saveScheduleAndGenerateScheduledSessions(tutorialGroupsConfiguration, tutorialGroup, tutorialGroupSchedule)); } } diff --git a/src/test/java/de/tum/cit/aet/artemis/communication/ForwardedMessageResourceIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/communication/ForwardedMessageResourceIntegrationTest.java index 650be50328f4..d3326445393b 100644 --- a/src/test/java/de/tum/cit/aet/artemis/communication/ForwardedMessageResourceIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/communication/ForwardedMessageResourceIntegrationTest.java @@ -62,8 +62,7 @@ void setUp() throws IOException { testUser = userUtilService.getUserByLogin(TEST_PREFIX + "student1"); - ConversationFactory factory = new ConversationFactory(); - Conversation conversation = factory.generatePublicChannel(exampleCourse, "Test ForwardedMessage Channel", true); + Conversation conversation = ConversationFactory.generatePublicChannel(exampleCourse, "Test ForwardedMessage Channel", true); conversationRepository.save(conversation); testPost = ConversationFactory.createBasicPost(1, testUser); diff --git a/src/test/java/de/tum/cit/aet/artemis/core/connector/AeolusRequestMockProvider.java b/src/test/java/de/tum/cit/aet/artemis/core/connector/AeolusRequestMockProvider.java index 56ad679806e2..f52a56b04626 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/connector/AeolusRequestMockProvider.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/connector/AeolusRequestMockProvider.java @@ -40,7 +40,7 @@ public class AeolusRequestMockProvider { private MockRestServiceServer mockServer; - private ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectMapper objectMapper = new ObjectMapper(); /** * Constructor for the AeolusRequestMockProvider 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 b6c215590c21..faf8d852367a 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 @@ -1784,7 +1784,6 @@ void getParticipation_quizExerciseStartedAndSubmissionAllowed(QuizMode quizMode) request.postWithResponseBody("/api/quiz-exercises/" + quizEx.getId() + "/join", new QuizBatchJoinDTO(batch.getPassword()), QuizBatch.class, HttpStatus.OK); } var participation = request.postWithResponseBody("/api/quiz-exercises/" + quizEx.getId() + "/start-participation", null, StudentParticipation.class, HttpStatus.OK); - ; assertThat(participation.getExercise()).as("Participation contains exercise").isEqualTo(quizEx); assertThat(participation.getResults()).as("New result was added to the participation").hasSize(1); assertThat(participation.getInitializationState()).as("Participation was initialized").isEqualTo(InitializationState.INITIALIZED); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/GitServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/GitServiceTest.java index 33119358608f..509bdf6690a3 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/GitServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/GitServiceTest.java @@ -296,7 +296,7 @@ void testListFilesAndFolders() { assertThat(map).hasSize(5).containsEntry(new File(gitUtilService.getFile(GitUtilService.REPOS.LOCAL, GitUtilService.FILES.FILE1), localRepo), FileType.FILE) .containsEntry(new File(gitUtilService.getFile(GitUtilService.REPOS.LOCAL, GitUtilService.FILES.FILE2), localRepo), FileType.FILE) .containsEntry(new File(gitUtilService.getFile(GitUtilService.REPOS.LOCAL, GitUtilService.FILES.FILE3), localRepo), FileType.FILE) - .containsEntry(new File(gitUtilService.getFile(GitUtilService.REPOS.LOCAL, GitUtilService.FILES.FILE4.toString() + ".jar"), localRepo), FileType.FILE) + .containsEntry(new File(gitUtilService.getFile(GitUtilService.REPOS.LOCAL, GitUtilService.FILES.FILE4 + ".jar"), localRepo), FileType.FILE) .containsEntry(new File(localRepo.getLocalPath().toFile(), localRepo), FileType.FOLDER); } @@ -310,7 +310,7 @@ void testListFilesAndFoldersAndOmitBinary() { .containsEntry(new File(gitUtilService.getFile(GitUtilService.REPOS.LOCAL, GitUtilService.FILES.FILE2), localRepo), FileType.FILE) .containsEntry(new File(gitUtilService.getFile(GitUtilService.REPOS.LOCAL, GitUtilService.FILES.FILE3), localRepo), FileType.FILE) .containsEntry(new File(localRepo.getLocalPath().toFile(), localRepo), FileType.FOLDER) - .doesNotContainKey(new File(gitUtilService.getFile(GitUtilService.REPOS.LOCAL, GitUtilService.FILES.FILE4.toString() + ".jar"), localRepo)); + .doesNotContainKey(new File(gitUtilService.getFile(GitUtilService.REPOS.LOCAL, GitUtilService.FILES.FILE4 + ".jar"), localRepo)); } @Test diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/util/GitUtilService.java b/src/test/java/de/tum/cit/aet/artemis/programming/util/GitUtilService.java index 2c08f55dda79..a629b0649446 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/util/GitUtilService.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/util/GitUtilService.java @@ -96,7 +96,7 @@ public void initRepo(String defaultBranch) { remotePath.resolve(FILES.FILE1.toString()).toFile().createNewFile(); remotePath.resolve(FILES.FILE2.toString()).toFile().createNewFile(); remotePath.resolve(FILES.FILE3.toString()).toFile().createNewFile(); - remotePath.resolve(FILES.FILE4.toString() + ".jar").toFile().createNewFile(); + remotePath.resolve(FILES.FILE4 + ".jar").toFile().createNewFile(); remoteGit.add().addFilepattern(".").call(); GitService.commit(remoteGit).setMessage("initial commit").call(); diff --git a/src/test/java/de/tum/cit/aet/artemis/shared/architecture/ArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/shared/architecture/ArchitectureTest.java index d2d6d431a9de..afc9719fcddb 100644 --- a/src/test/java/de/tum/cit/aet/artemis/shared/architecture/ArchitectureTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/shared/architecture/ArchitectureTest.java @@ -17,7 +17,6 @@ import static com.tngtech.archunit.core.domain.properties.HasOwner.Predicates.With.owner; import static com.tngtech.archunit.lang.ConditionEvent.createMessage; import static com.tngtech.archunit.lang.SimpleConditionEvent.violated; -import static com.tngtech.archunit.lang.conditions.ArchConditions.callMethod; import static com.tngtech.archunit.lang.conditions.ArchPredicates.are; import static com.tngtech.archunit.lang.conditions.ArchPredicates.have; import static com.tngtech.archunit.lang.conditions.ArchPredicates.is; diff --git a/src/test/java/de/tum/cit/aet/artemis/shared/architecture/module/AbstractModuleResourceArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/shared/architecture/module/AbstractModuleResourceArchitectureTest.java index 5442e6d26c4b..f1f5ae353cca 100644 --- a/src/test/java/de/tum/cit/aet/artemis/shared/architecture/module/AbstractModuleResourceArchitectureTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/shared/architecture/module/AbstractModuleResourceArchitectureTest.java @@ -2,7 +2,6 @@ import static com.tngtech.archunit.lang.ConditionEvent.createMessage; import static com.tngtech.archunit.lang.SimpleConditionEvent.violated; -import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods; import java.lang.annotation.Annotation; From 4d095dd8aa20aa989b5d59b4a84d9b6f53d8c417 Mon Sep 17 00:00:00 2001 From: Michal Kawka <73854755+coolchock@users.noreply.github.com> Date: Tue, 25 Feb 2025 20:01:35 +0100 Subject: [PATCH 12/17] General: Fix missing labels in the user interface (#10400) --- .../course-competencies-relation-modal.component.ts | 3 ++- ...mport-all-course-competencies-modal.component.ts | 3 ++- .../competencies/edit/edit-competency.component.ts | 3 ++- .../edit/edit-prerequisite.component.ts | 3 ++- .../forms/competency/competency-form.component.ts | 3 ++- .../prerequisite/prerequisite-form.component.ts | 3 ++- .../judgement-of-learning-rating.component.ts | 3 ++- .../learning-paths-table.component.ts | 3 ++- ...auxiliary-repository-buttons-detail.component.ts | 3 ++- .../programming-diff-report-detail.component.ts | 3 ++- .../exam/participate/exam-bar/exam-bar.component.ts | 3 ++- .../theia/programming-exercise-theia.component.ts | 3 ++- ...e-repository-and-build-plan-details.component.ts | 3 ++- .../header/code-editor-header.component.ts | 3 ++- .../monaco/code-editor-monaco.component.ts | 9 ++++++++- .../pdf-preview-enlarged-canvas.component.ts | 3 ++- .../build-agent-details.component.ts | 13 ++++++++++++- .../posting-content-part.components.ts | 3 ++- .../standardized-competency-filter.component.ts | 3 ++- 19 files changed, 54 insertions(+), 19 deletions(-) diff --git a/src/main/webapp/app/course/competencies/components/course-competencies-relation-modal/course-competencies-relation-modal.component.ts b/src/main/webapp/app/course/competencies/components/course-competencies-relation-modal/course-competencies-relation-modal.component.ts index b7a7f3b237cb..0995471fe6fa 100644 --- a/src/main/webapp/app/course/competencies/components/course-competencies-relation-modal/course-competencies-relation-modal.component.ts +++ b/src/main/webapp/app/course/competencies/components/course-competencies-relation-modal/course-competencies-relation-modal.component.ts @@ -7,10 +7,11 @@ import { onError } from 'app/shared/util/global.utils'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { CourseCompetencyRelationFormComponent } from 'app/course/competencies/components/course-competency-relation-form/course-competency-relation-form.component'; import { CourseCompetenciesRelationGraphComponent } from '../course-competencies-relation-graph/course-competencies-relation-graph.component'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; @Component({ selector: 'jhi-course-competencies-relation-modal', - imports: [CourseCompetenciesRelationGraphComponent, CourseCompetencyRelationFormComponent], + imports: [CourseCompetenciesRelationGraphComponent, CourseCompetencyRelationFormComponent, TranslateDirective], templateUrl: './course-competencies-relation-modal.component.html', styleUrl: './course-competencies-relation-modal.component.scss', }) diff --git a/src/main/webapp/app/course/competencies/components/import-all-course-competencies-modal/import-all-course-competencies-modal.component.ts b/src/main/webapp/app/course/competencies/components/import-all-course-competencies-modal/import-all-course-competencies-modal.component.ts index cfd0c68a5580..1713d7ba9a98 100644 --- a/src/main/webapp/app/course/competencies/components/import-all-course-competencies-modal/import-all-course-competencies-modal.component.ts +++ b/src/main/webapp/app/course/competencies/components/import-all-course-competencies-modal/import-all-course-competencies-modal.component.ts @@ -13,6 +13,7 @@ import { } from 'app/course/competencies/components/import-course-competencies-settings/import-course-competencies-settings.component'; import { CourseCompetencyImportOptionsDTO } from 'app/entities/competency.model'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; const tableColumns: Column[] = [ { @@ -36,7 +37,7 @@ export interface ImportAllCourseCompetenciesResult { @Component({ selector: 'jhi-import-all-course-competencies-modal', - imports: [ImportTableComponent, ImportCourseCompetenciesSettingsComponent, FaIconComponent], + imports: [ImportTableComponent, ImportCourseCompetenciesSettingsComponent, FaIconComponent, TranslateDirective], providers: [ { provide: PagingService, diff --git a/src/main/webapp/app/course/competencies/edit/edit-competency.component.ts b/src/main/webapp/app/course/competencies/edit/edit-competency.component.ts index cafdd95f4a1b..c323f8878ce8 100644 --- a/src/main/webapp/app/course/competencies/edit/edit-competency.component.ts +++ b/src/main/webapp/app/course/competencies/edit/edit-competency.component.ts @@ -9,11 +9,12 @@ import { CompetencyFormComponent } from 'app/course/competencies/forms/competenc import { EditCourseCompetencyComponent } from 'app/course/competencies/edit/edit-course-competency.component'; import { CourseCompetencyFormData } from 'app/course/competencies/forms/course-competency-form.component'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; @Component({ selector: 'jhi-edit-competency', templateUrl: './edit-competency.component.html', - imports: [CompetencyFormComponent], + imports: [CompetencyFormComponent, TranslateDirective], }) export class EditCompetencyComponent extends EditCourseCompetencyComponent implements OnInit { private competencyService = inject(CompetencyService); diff --git a/src/main/webapp/app/course/competencies/edit/edit-prerequisite.component.ts b/src/main/webapp/app/course/competencies/edit/edit-prerequisite.component.ts index 9d111272b8d6..7020a1dd8b75 100644 --- a/src/main/webapp/app/course/competencies/edit/edit-prerequisite.component.ts +++ b/src/main/webapp/app/course/competencies/edit/edit-prerequisite.component.ts @@ -9,11 +9,12 @@ import { PrerequisiteFormComponent } from 'app/course/competencies/forms/prerequ import { Prerequisite } from 'app/entities/prerequisite.model'; import { PrerequisiteService } from 'app/course/competencies/prerequisite.service'; import { CourseCompetencyFormData } from 'app/course/competencies/forms/course-competency-form.component'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; @Component({ selector: 'jhi-edit-prerequisite', templateUrl: './edit-prerequisite.component.html', - imports: [PrerequisiteFormComponent], + imports: [PrerequisiteFormComponent, TranslateDirective], }) export class EditPrerequisiteComponent extends EditCourseCompetencyComponent implements OnInit { private prerequisiteService = inject(PrerequisiteService); diff --git a/src/main/webapp/app/course/competencies/forms/competency/competency-form.component.ts b/src/main/webapp/app/course/competencies/forms/competency/competency-form.component.ts index 0e8c774ea239..32faabc86ef0 100644 --- a/src/main/webapp/app/course/competencies/forms/competency/competency-form.component.ts +++ b/src/main/webapp/app/course/competencies/forms/competency/competency-form.component.ts @@ -5,12 +5,13 @@ import { CommonCourseCompetencyFormComponent } from 'app/course/competencies/for import { Competency } from 'app/entities/competency.model'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; @Component({ selector: 'jhi-competency-form', templateUrl: './competency-form.component.html', styleUrls: ['./competency-form.component.scss'], - imports: [CommonCourseCompetencyFormComponent, FormsModule, ReactiveFormsModule, FontAwesomeModule], + imports: [CommonCourseCompetencyFormComponent, FormsModule, ReactiveFormsModule, FontAwesomeModule, TranslateDirective], }) export class CompetencyFormComponent extends CourseCompetencyFormComponent implements OnInit, OnChanges { @Input() formData: CourseCompetencyFormData = { diff --git a/src/main/webapp/app/course/competencies/forms/prerequisite/prerequisite-form.component.ts b/src/main/webapp/app/course/competencies/forms/prerequisite/prerequisite-form.component.ts index 17260bca52e3..f6761d969776 100644 --- a/src/main/webapp/app/course/competencies/forms/prerequisite/prerequisite-form.component.ts +++ b/src/main/webapp/app/course/competencies/forms/prerequisite/prerequisite-form.component.ts @@ -6,12 +6,13 @@ import { CourseCompetencyType } from 'app/entities/competency.model'; import { Prerequisite } from 'app/entities/prerequisite.model'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; @Component({ selector: 'jhi-prerequisite-form', templateUrl: './prerequisite-form.component.html', styleUrls: ['./prerequisite-form.component.scss'], - imports: [CommonCourseCompetencyFormComponent, FormsModule, ReactiveFormsModule, FontAwesomeModule], + imports: [CommonCourseCompetencyFormComponent, FormsModule, ReactiveFormsModule, FontAwesomeModule, TranslateDirective], }) export class PrerequisiteFormComponent extends CourseCompetencyFormComponent implements OnInit, OnChanges { @Input() formData: CourseCompetencyFormData = { diff --git a/src/main/webapp/app/course/competencies/judgement-of-learning-rating/judgement-of-learning-rating.component.ts b/src/main/webapp/app/course/competencies/judgement-of-learning-rating/judgement-of-learning-rating.component.ts index 7283cd01b3dc..c4d991736842 100644 --- a/src/main/webapp/app/course/competencies/judgement-of-learning-rating/judgement-of-learning-rating.component.ts +++ b/src/main/webapp/app/course/competencies/judgement-of-learning-rating/judgement-of-learning-rating.component.ts @@ -5,10 +5,11 @@ import { AlertService } from 'app/core/util/alert.service'; import { CourseCompetencyService } from 'app/course/competencies/course-competency.service'; import { HelpIconComponent } from 'app/shared/components/help-icon.component'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; @Component({ selector: 'jhi-judgement-of-learning-rating', - imports: [StarRatingComponent, HelpIconComponent], + imports: [StarRatingComponent, HelpIconComponent, TranslateDirective], templateUrl: './judgement-of-learning-rating.component.html', }) export class JudgementOfLearningRatingComponent { diff --git a/src/main/webapp/app/course/learning-paths/components/learning-paths-table/learning-paths-table.component.ts b/src/main/webapp/app/course/learning-paths/components/learning-paths-table/learning-paths-table.component.ts index 37b73a6f993c..a18b04f41ee4 100644 --- a/src/main/webapp/app/course/learning-paths/components/learning-paths-table/learning-paths-table.component.ts +++ b/src/main/webapp/app/course/learning-paths/components/learning-paths-table/learning-paths-table.component.ts @@ -11,6 +11,7 @@ import { BaseApiHttpService } from 'app/course/learning-paths/services/base-api- import { FormsModule } from '@angular/forms'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; enum TableColumn { ID = 'ID', @@ -22,7 +23,7 @@ enum TableColumn { @Component({ selector: 'jhi-learning-paths-table', changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgbPaginationModule, NgbTypeaheadModule, FormsModule, FontAwesomeModule, ArtemisTranslatePipe], + imports: [NgbPaginationModule, NgbTypeaheadModule, FormsModule, FontAwesomeModule, ArtemisTranslatePipe, TranslateDirective], templateUrl: './learning-paths-table.component.html', styleUrls: ['./learning-paths-table.component.scss', '../../pages/learning-path-instructor-page/learning-path-instructor-page.component.scss'], }) diff --git a/src/main/webapp/app/detail-overview-list/components/programming-auxiliary-repository-buttons-detail/programming-auxiliary-repository-buttons-detail.component.ts b/src/main/webapp/app/detail-overview-list/components/programming-auxiliary-repository-buttons-detail/programming-auxiliary-repository-buttons-detail.component.ts index e385b12d7a9f..abb35e0a77c1 100644 --- a/src/main/webapp/app/detail-overview-list/components/programming-auxiliary-repository-buttons-detail/programming-auxiliary-repository-buttons-detail.component.ts +++ b/src/main/webapp/app/detail-overview-list/components/programming-auxiliary-repository-buttons-detail/programming-auxiliary-repository-buttons-detail.component.ts @@ -7,11 +7,12 @@ import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { CodeButtonComponent } from 'app/shared/components/code-button/code-button.component'; import { ProgrammingExerciseInstructorRepoDownloadComponent } from 'app/exercises/programming/shared/actions/programming-exercise-instructor-repo-download.component'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; @Component({ selector: 'jhi-programming-auxiliary-repository-buttons-detail', templateUrl: 'programming-auxiliary-repository-buttons-detail.component.html', - imports: [RouterModule, NgbTooltipModule, FontAwesomeModule, CodeButtonComponent, ProgrammingExerciseInstructorRepoDownloadComponent], + imports: [RouterModule, NgbTooltipModule, FontAwesomeModule, CodeButtonComponent, ProgrammingExerciseInstructorRepoDownloadComponent, TranslateDirective], }) export class ProgrammingAuxiliaryRepositoryButtonsDetailComponent { @Input() detail: ProgrammingAuxiliaryRepositoryButtonsDetail; diff --git a/src/main/webapp/app/detail-overview-list/components/programming-diff-report-detail/programming-diff-report-detail.component.ts b/src/main/webapp/app/detail-overview-list/components/programming-diff-report-detail/programming-diff-report-detail.component.ts index 1acdf6249397..35ca5ff9f7d1 100644 --- a/src/main/webapp/app/detail-overview-list/components/programming-diff-report-detail/programming-diff-report-detail.component.ts +++ b/src/main/webapp/app/detail-overview-list/components/programming-diff-report-detail/programming-diff-report-detail.component.ts @@ -9,11 +9,12 @@ import { GitDiffReportModalComponent } from 'app/exercises/programming/git-diff- import { NgbModal, NgbModalRef, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { GitDiffLineStatComponent } from 'app/exercises/programming/git-diff-report/git-diff-line-stat.component'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; @Component({ selector: 'jhi-programming-diff-report-detail', templateUrl: 'programming-diff-report-detail.component.html', - imports: [GitDiffLineStatComponent, ArtemisTranslatePipe, NgbTooltipModule, ButtonComponent], + imports: [GitDiffLineStatComponent, ArtemisTranslatePipe, NgbTooltipModule, ButtonComponent, TranslateDirective], }) export class ProgrammingDiffReportDetailComponent implements OnDestroy { protected readonly FeatureToggle = FeatureToggle; diff --git a/src/main/webapp/app/exam/participate/exam-bar/exam-bar.component.ts b/src/main/webapp/app/exam/participate/exam-bar/exam-bar.component.ts index 1dd2ff913a56..c3002791ac22 100644 --- a/src/main/webapp/app/exam/participate/exam-bar/exam-bar.component.ts +++ b/src/main/webapp/app/exam/participate/exam-bar/exam-bar.component.ts @@ -10,10 +10,11 @@ import { StudentExam } from 'app/entities/student-exam.model'; import { ExamTimerComponent } from 'app/exam/participate/timer/exam-timer.component'; import { ExamLiveEventsButtonComponent } from 'app/exam/participate/events/exam-live-events-button.component'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; @Component({ selector: 'jhi-exam-bar', - imports: [CommonModule, ExamTimerComponent, ExamLiveEventsButtonComponent, FontAwesomeModule], + imports: [CommonModule, ExamTimerComponent, ExamLiveEventsButtonComponent, FontAwesomeModule, TranslateDirective], templateUrl: './exam-bar.component.html', styleUrl: './exam-bar.component.scss', }) diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/theia/programming-exercise-theia.component.ts b/src/main/webapp/app/exercises/programming/manage/update/update-components/theia/programming-exercise-theia.component.ts index 55867d4c7b1f..c45b76101069 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/update-components/theia/programming-exercise-theia.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/theia/programming-exercise-theia.component.ts @@ -4,12 +4,13 @@ import { FormsModule } from '@angular/forms'; import { ProgrammingExercise, ProgrammingLanguage } from 'app/entities/programming/programming-exercise.model'; import { ProgrammingExerciseCreationConfig } from 'app/exercises/programming/manage/update/programming-exercise-creation-config'; import { TheiaService } from 'app/exercises/programming/shared/service/theia.service'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; @Component({ selector: 'jhi-programming-exercise-theia', templateUrl: './programming-exercise-theia.component.html', styleUrls: ['../../../programming-exercise-form.scss'], - imports: [FormsModule, KeyValuePipe], + imports: [FormsModule, KeyValuePipe, TranslateDirective], }) export class ProgrammingExerciseTheiaComponent implements OnChanges { private theiaService = inject(TheiaService); diff --git a/src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component.ts b/src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component.ts index e76efaa6a5f1..b47d852c72c6 100644 --- a/src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component.ts +++ b/src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component.ts @@ -10,12 +10,13 @@ import { ProgrammingExerciseBuildPlanCheckoutDirectoriesComponent } from 'app/ex import { BuildPlanCheckoutDirectoriesDTO } from 'app/entities/programming/build-plan-checkout-directories-dto'; import { HelpIconComponent } from 'app/shared/components/help-icon.component'; import { CommonModule } from '@angular/common'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; @Component({ selector: 'jhi-programming-exercise-repository-and-build-plan-details', templateUrl: './programming-exercise-repository-and-build-plan-details.component.html', styleUrls: ['../../manage/programming-exercise-form.scss'], - imports: [ProgrammingExerciseBuildPlanCheckoutDirectoriesComponent, HelpIconComponent, CommonModule], + imports: [ProgrammingExerciseBuildPlanCheckoutDirectoriesComponent, HelpIconComponent, CommonModule, TranslateDirective], }) export class ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent implements OnInit, OnChanges, OnDestroy { private programmingExerciseService = inject(ProgrammingExerciseService); diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/header/code-editor-header.component.ts b/src/main/webapp/app/exercises/programming/shared/code-editor/header/code-editor-header.component.ts index be2c845d5aee..b5dd5762a6a7 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/header/code-editor-header.component.ts +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/header/code-editor-header.component.ts @@ -6,11 +6,12 @@ import { faCircleNotch, faGear } from '@fortawesome/free-solid-svg-icons'; import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'; import { MAX_TAB_SIZE } from 'app/shared/monaco-editor/monaco-editor.component'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; @Component({ selector: 'jhi-code-editor-header', templateUrl: './code-editor-header.component.html', - imports: [NgbDropdown, ArtemisTranslatePipe, FormsModule, FontAwesomeModule], + imports: [NgbDropdown, ArtemisTranslatePipe, FormsModule, FontAwesomeModule, TranslateDirective], changeDetection: ChangeDetectionStrategy.OnPush, }) export class CodeEditorHeaderComponent { diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/monaco/code-editor-monaco.component.ts b/src/main/webapp/app/exercises/programming/shared/code-editor/monaco/code-editor-monaco.component.ts index b955e87aefbb..8993073bb317 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/monaco/code-editor-monaco.component.ts +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/monaco/code-editor-monaco.component.ts @@ -39,6 +39,7 @@ import { MonacoEditorLineHighlight } from 'app/shared/monaco-editor/model/monaco import { FileTypeService } from 'app/exercises/programming/shared/service/file-type.service'; import { EditorPosition } from 'app/shared/monaco-editor/model/actions/monaco-editor.util'; import { CodeEditorHeaderComponent } from 'app/exercises/programming/shared/code-editor/header/code-editor-header.component'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; type FileSession = { [fileName: string]: { code: string; cursor: EditorPosition; scrollTop: number; loadingError: boolean } }; type FeedbackWithLineAndReference = Feedback & { line: number; reference: string }; @@ -48,7 +49,13 @@ export type Annotation = { fileName: string; row: number; column: number; text: templateUrl: './code-editor-monaco.component.html', styleUrls: ['./code-editor-monaco.component.scss'], encapsulation: ViewEncapsulation.None, - imports: [MonacoEditorComponent, CodeEditorHeaderComponent, CodeEditorTutorAssessmentInlineFeedbackSuggestionComponent, CodeEditorTutorAssessmentInlineFeedbackComponent], + imports: [ + MonacoEditorComponent, + CodeEditorHeaderComponent, + CodeEditorTutorAssessmentInlineFeedbackSuggestionComponent, + CodeEditorTutorAssessmentInlineFeedbackComponent, + TranslateDirective, + ], providers: [RepositoryFileService], changeDetection: ChangeDetectionStrategy.OnPush, }) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts index bd803a3f8364..cb2ef3af12b0 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts @@ -1,4 +1,5 @@ import { Component, ElementRef, HostListener, effect, input, output, signal, viewChild } from '@angular/core'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; type NavigationDirection = 'next' | 'prev'; @@ -6,7 +7,7 @@ type NavigationDirection = 'next' | 'prev'; selector: 'jhi-pdf-preview-enlarged-canvas-component', templateUrl: './pdf-preview-enlarged-canvas.component.html', styleUrls: ['./pdf-preview-enlarged-canvas.component.scss'], - imports: [], + imports: [TranslateDirective], }) export class PdfPreviewEnlargedCanvasComponent { enlargedContainer = viewChild.required>('enlargedContainer'); diff --git a/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.ts b/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.ts index 8607cddd66e5..048d6efc359a 100644 --- a/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.ts +++ b/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.ts @@ -16,12 +16,23 @@ import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { CommonModule } from '@angular/common'; import { DataTableComponent } from 'app/shared/data-table/data-table.component'; import { ResultComponent } from 'app/exercises/shared/result/result.component'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; @Component({ selector: 'jhi-build-agent-details', templateUrl: './build-agent-details.component.html', styleUrl: './build-agent-details.component.scss', - imports: [NgxDatatableModule, DataTableComponent, ArtemisDurationFromSecondsPipe, ArtemisDatePipe, FontAwesomeModule, RouterModule, CommonModule, ResultComponent], + imports: [ + NgxDatatableModule, + DataTableComponent, + ArtemisDurationFromSecondsPipe, + ArtemisDatePipe, + FontAwesomeModule, + RouterModule, + CommonModule, + ResultComponent, + TranslateDirective, + ], }) export class BuildAgentDetailsComponent implements OnInit, OnDestroy { private readonly websocketService = inject(WebsocketService); diff --git a/src/main/webapp/app/shared/metis/posting-content/posting-content-part/posting-content-part.components.ts b/src/main/webapp/app/shared/metis/posting-content/posting-content-part/posting-content-part.components.ts index 81822a637666..b976c3e33e8b 100644 --- a/src/main/webapp/app/shared/metis/posting-content/posting-content-part/posting-content-part.components.ts +++ b/src/main/webapp/app/shared/metis/posting-content/posting-content-part/posting-content-part.components.ts @@ -24,12 +24,13 @@ import { AccountService } from 'app/core/auth/account.service'; import { RouterLink } from '@angular/router'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { HtmlForPostingMarkdownPipe } from 'app/shared/pipes/html-for-posting-markdown.pipe'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; @Component({ selector: 'jhi-posting-content-part', templateUrl: './posting-content-part.component.html', styleUrls: ['./../../metis.component.scss'], - imports: [RouterLink, FaIconComponent, HtmlForPostingMarkdownPipe], + imports: [RouterLink, FaIconComponent, HtmlForPostingMarkdownPipe, TranslateDirective], }) export class PostingContentPartComponent implements OnInit, OnChanges { private fileService = inject(FileService); diff --git a/src/main/webapp/app/shared/standardized-competencies/standardized-competency-filter.component.ts b/src/main/webapp/app/shared/standardized-competencies/standardized-competency-filter.component.ts index eed83bae4790..807781166e54 100644 --- a/src/main/webapp/app/shared/standardized-competencies/standardized-competency-filter.component.ts +++ b/src/main/webapp/app/shared/standardized-competencies/standardized-competency-filter.component.ts @@ -2,11 +2,12 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angu import { KnowledgeAreaDTO } from 'app/entities/competency/standardized-competency.model'; import { Subject, debounceTime } from 'rxjs'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; @Component({ selector: 'jhi-standardized-competency-filter', templateUrl: './standardized-competency-filter.component.html', - imports: [FormsModule, ReactiveFormsModule], + imports: [FormsModule, ReactiveFormsModule, TranslateDirective], }) export class StandardizedCompetencyFilterComponent implements OnInit, OnDestroy { @Input() competencyTitleFilter: string; From f60776573b0da500994590826517a486c5ad5e3c Mon Sep 17 00:00:00 2001 From: Julian Waluschyk <37155504+julian-wls@users.noreply.github.com> Date: Tue, 25 Feb 2025 21:39:45 +0100 Subject: [PATCH 13/17] Development: Update communication feature availability list in user documentation (#10378) --- docs/user/communication.rst | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/docs/user/communication.rst b/docs/user/communication.rst index 026785e4861b..b79c311c76a3 100644 --- a/docs/user/communication.rst +++ b/docs/user/communication.rst @@ -171,6 +171,8 @@ Available features on each platform +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ | Reply in Thread | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| See who reacted to a post | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ | Copy Text | | |NOT PLANNED| | |AVAILABLE| | |AVAILABLE| | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ | Pin Messages | | Groups: group creators | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | @@ -181,13 +183,13 @@ Available features on each platform +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ | Edit Message | Authors only | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ -| Save Message for later | | |AVAILABLE| | |UNAVAILABLE| | |PLANNED| | +| Save Message for later | | |AVAILABLE| | |PLANNED| | |AVAILABLE| | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ -| Forward Messages | | |WIP| | |UNAVAILABLE| | |PLANNED| | +| Forward Messages | | |AVAILABLE| | |PLANNED| | |WIP| | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ | Resolve Messages | At least tutor and authors | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ -| Post action bar (thread view) | ||NOT PLANNED| | |AVAILABLE| | |WIP| | +| Post action bar (thread view) | ||NOT PLANNED| | |AVAILABLE| | |AVAILABLE| | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ | | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ @@ -197,11 +199,11 @@ Available features on each platform +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ | Reference channels, lectures and exercises | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ -| Tag FAQ | | |AVAILABLE| | |WIP| | |PLANNED| | +| Tag FAQ | | |AVAILABLE| | |AVAILABLE| | |PLANNED| | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ | Basic formatting (underline, bold, italic) | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ -| Strikethrough formatting | | |AVAILABLE| | |UNAVAILABLE| | |PLANNED| | +| Strikethrough formatting | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ | Preview | | |AVAILABLE| | |UNAVAILABLE| | |AVAILABLE| | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ @@ -215,11 +217,11 @@ Available features on each platform +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ | **Messages** | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ -| Profile pictures | | |AVAILABLE| | |AVAILABLE| | |WIP| | +| Profile pictures | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ | Show if message was edited, resolved or pinned | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ -| | Render links to exercises, lectures, other chats, | | |AVAILABLE| | |AVAILABLE| | |WIP| | +| | Render links to exercises, lectures, other chats, | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | | | lecture-units, slides, lecture-attachment with | | | | | | | correct icon | | | | | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ @@ -231,17 +233,17 @@ Available features on each platform +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ | Render links to uploaded files | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ -| Filter messages (unresolved, own, reacted) | | |AVAILABLE| | |AVAILABLE| | |UNAVAILABLE| | +| Filter messages (unresolved, own, reacted) | | |AVAILABLE| | |AVAILABLE| | |PLANNED| | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ -| Sort messages (ascending, descending) | | |AVAILABLE| | |NOT PLANNED| | |UNAVAILABLE| | +| Sort messages (ascending, descending) | | |AVAILABLE| | |NOT PLANNED| | |NOT PLANNED| | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ -| Search for messages in chat | | |UNAVAILABLE| | |UNAVAILABLE| | |AVAILABLE| | +| Search for messages in chat | | |AVAILABLE| | |UNAVAILABLE| | |AVAILABLE| | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ | Search for messages across all chats | | |AVAILABLE| | |UNAVAILABLE| | |UNAVAILABLE| | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ -| Open Profile info by clicking profile picture | | |PLANNED| | |AVAILABLE| | |WIP| | +| Open Profile info by clicking profile picture | | |PLANNED| | |AVAILABLE| | |AVAILABLE| | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ -| Start a conversation from Profile | | |WIP| | |AVAILABLE| | |PLANNED| | +| Start a conversation from Profile | | |WIP| | |AVAILABLE| | |AVAILABLE| | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ | | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ @@ -252,9 +254,9 @@ Available features on each platform +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ | Open sent images full-screen | | |AVAILABLE| | |AVAILABLE| | |PLANNED| | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ -| Download sent images | | |AVAILABLE| | |PLANNED| | |UNAVAILABLE| | +| Download sent images | | |AVAILABLE| | |AVAILABLE| | |UNAVAILABLE| | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ -| View and download attachments | | |AVAILABLE| | |PLANNED| | |PLANNED| | +| View and download attachments | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ | | +------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ @@ -333,6 +335,7 @@ Available features on each platform - Leave chat option is available on the web app for groups only, on iOS for groups and non course-wide channels, and on Android for channels, groups, and DMs. - Creating a group chat on iOS and Android can be achieved via the 'Create Chat' option. It becomes a group when more than one user is added. - Downloading sent images in the chat is only available through the browser option on the web app. + - Seeing who reacted to a post is available when hovering over a reaction on the web app. Features for Users ------------------ From 95b65db9de09c266e50e488cc52adb37a50b22c0 Mon Sep 17 00:00:00 2001 From: Michal Kawka <73854755+coolchock@users.noreply.github.com> Date: Tue, 25 Feb 2025 21:42:25 +0100 Subject: [PATCH 14/17] Text exercises: Fix an issue in the text exercise submission view (#10390) --- .../text/web/TextExerciseResource.java | 4 +- .../header-participation-page.component.ts | 10 ++- .../participate/text-editor.component.html | 2 +- .../text/participate/text-editor.component.ts | 2 +- .../text/TextAssessmentIntegrationTest.java | 64 +++++++++++++++++++ ...eader-participation-page.component.spec.ts | 14 ++++ 6 files changed, 86 insertions(+), 10 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/text/web/TextExerciseResource.java b/src/main/java/de/tum/cit/aet/artemis/text/web/TextExerciseResource.java index 95b40c014656..7bc6c5405fbd 100644 --- a/src/main/java/de/tum/cit/aet/artemis/text/web/TextExerciseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/text/web/TextExerciseResource.java @@ -423,7 +423,7 @@ public ResponseEntity getDataForTextEditor(@PathVariable L participation.setResults(new HashSet<>(results)); } - if (!ExerciseDateService.isAfterAssessmentDueDate(textExercise)) { + if (!ExerciseDateService.isAfterAssessmentDueDate(textExercise) && !authCheckService.isAtLeastTeachingAssistantForExercise(textExercise, user)) { // We want to have the preliminary feedback before the assessment due date too Set athenaResults = participation.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA) .collect(Collectors.toSet()); @@ -440,7 +440,7 @@ public ResponseEntity getDataForTextEditor(@PathVariable L // set reference to participation to null, since we are already inside a participation textSubmission.setParticipation(null); - if (!ExerciseDateService.isAfterAssessmentDueDate(textExercise)) { + if (!ExerciseDateService.isAfterAssessmentDueDate(textExercise) && !authCheckService.isAtLeastTeachingAssistantForExercise(textExercise, user)) { // We want to have the preliminary feedback before the assessment due date too List athenaResults = submission.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA).toList(); textSubmission.setResults(athenaResults); diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/header-participation-page.component.ts b/src/main/webapp/app/exercises/shared/exercise-headers/header-participation-page.component.ts index 3a181e33f0f4..fff7f0bec69f 100644 --- a/src/main/webapp/app/exercises/shared/exercise-headers/header-participation-page.component.ts +++ b/src/main/webapp/app/exercises/shared/exercise-headers/header-participation-page.component.ts @@ -14,6 +14,7 @@ import { SubmissionResultStatusComponent } from 'app/overview/submission-result- import { ArtemisDatePipe } from 'app/shared/pipes/artemis-date.pipe'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { ArtemisTimeAgoPipe } from 'app/shared/pipes/artemis-time-ago.pipe'; +import { getLatestResultOfStudentParticipation } from 'app/exercises/shared/participation/participation.utils'; @Component({ selector: 'jhi-header-participation-page', @@ -75,12 +76,9 @@ export class HeaderParticipationPageComponent implements OnInit, OnChanges { this.exerciseStatusBadge = hasExerciseDueDatePassed(this.exercise, this.participation) ? 'bg-danger' : 'bg-success'; this.exerciseCategories = this.exercise.categories || []; this.dueDate = getExerciseDueDate(this.exercise, this.participation); - if (this.participation?.results?.last()?.rated) { - this.achievedPoints = roundValueSpecifiedByCourseSettings( - // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain - (this.participation.results?.last()?.score! * this.exercise.maxPoints!) / 100, - getCourseFromExercise(this.exercise), - ); + const result = getLatestResultOfStudentParticipation(this.participation, false, true); + if (result?.rated) { + this.achievedPoints = roundValueSpecifiedByCourseSettings((result.score! * this.exercise.maxPoints!) / 100, getCourseFromExercise(this.exercise)); } } } diff --git a/src/main/webapp/app/exercises/text/participate/text-editor.component.html b/src/main/webapp/app/exercises/text/participate/text-editor.component.html index cc6935f7be57..95795c20cf13 100644 --- a/src/main/webapp/app/exercises/text/participate/text-editor.component.html +++ b/src/main/webapp/app/exercises/text/participate/text-editor.component.html @@ -115,7 +115,7 @@ /> } } @else { - @if (!result.feedbacks?.length) { + @if (!result?.feedbacks?.length) {
Submission: diff --git a/src/main/webapp/app/exercises/text/participate/text-editor.component.ts b/src/main/webapp/app/exercises/text/participate/text-editor.component.ts index 3cdb3abcbcdd..c13a4599edf6 100644 --- a/src/main/webapp/app/exercises/text/participate/text-editor.component.ts +++ b/src/main/webapp/app/exercises/text/participate/text-editor.component.ts @@ -273,7 +273,7 @@ export class TextEditorComponent implements OnInit, OnDestroy, ComponentCanDeact } setLatestSubmissionResult(this.submission, getLatestSubmissionResult(this.submission)); - if (this.participation.results && (this.isAfterAssessmentDueDate || this.isAfterPublishDate || isAthenaAIResult(this.submission.latestResult!))) { + if (this.participation.results) { if (!this.submission?.results) { this.result = this.sortedHistoryResults.last()!; } else { diff --git a/src/test/java/de/tum/cit/aet/artemis/text/TextAssessmentIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/text/TextAssessmentIntegrationTest.java index 321a5f36bf61..e7ba8bd89071 100644 --- a/src/test/java/de/tum/cit/aet/artemis/text/TextAssessmentIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/text/TextAssessmentIntegrationTest.java @@ -587,6 +587,70 @@ void getDataForTextEditor_beforeAssessmentDueDate_athenaResults() throws Excepti assertThat(participation.getSubmissions().iterator().next().getResults()).hasSize(1); } + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void getDataForTextEditor_beforeAssessmentDueDate_athenaAndManualResults() throws Exception { + exerciseUtilService.updateAssessmentDueDate(textExercise.getId(), now().plusDays(1)); + + TextSubmission textSubmission = ParticipationFactory.generateTextSubmission("Some text", Language.ENGLISH, true); + textSubmission = textExerciseUtilService.saveTextSubmissionWithAthenaResult(textExercise, textSubmission, TEST_PREFIX + "student1"); + textSubmission = textExerciseUtilService.saveTextSubmissionWithResultAndAssessor(textExercise, textSubmission, TEST_PREFIX + "student1", TEST_PREFIX + "tutor1"); + + StudentParticipation participation = request.get("/api/text-editor/" + textSubmission.getParticipation().getId(), HttpStatus.OK, StudentParticipation.class); + + assertThat(participation.getResults()).hasSize(1); + assertThat(participation.getSubmissions()).hasSize(1); + assertThat(participation.getSubmissions().iterator().next().getResults()).hasSize(1); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void getDataForTextEditor_afterAssessmentDueDate_athenaAndManualResults() throws Exception { + assessmentDueDatePassed(); + + TextSubmission textSubmission = ParticipationFactory.generateTextSubmission("Some text", Language.ENGLISH, true); + textSubmission = textExerciseUtilService.saveTextSubmissionWithAthenaResult(textExercise, textSubmission, TEST_PREFIX + "student1"); + textSubmission = textExerciseUtilService.saveTextSubmissionWithResultAndAssessor(textExercise, textSubmission, TEST_PREFIX + "student1", TEST_PREFIX + "tutor1"); + + StudentParticipation participation = request.get("/api/text-editor/" + textSubmission.getParticipation().getId(), HttpStatus.OK, StudentParticipation.class); + + assertThat(participation.getResults()).hasSize(2); + assertThat(participation.getSubmissions()).hasSize(1); + assertThat(participation.getSubmissions().iterator().next().getResults()).hasSize(1); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "USER") + void getDataForTextEditor_asTutor_beforeAssessmentDueDate_athenaAndManualResults() throws Exception { + exerciseUtilService.updateAssessmentDueDate(textExercise.getId(), now().plusDays(1)); + + TextSubmission textSubmission = ParticipationFactory.generateTextSubmission("Some text", Language.ENGLISH, true); + textSubmission = textExerciseUtilService.saveTextSubmissionWithAthenaResult(textExercise, textSubmission, TEST_PREFIX + "student1"); + textSubmission = textExerciseUtilService.saveTextSubmissionWithResultAndAssessor(textExercise, textSubmission, TEST_PREFIX + "student1", TEST_PREFIX + "tutor1"); + + StudentParticipation participation = request.get("/api/text-editor/" + textSubmission.getParticipation().getId(), HttpStatus.OK, StudentParticipation.class); + + assertThat(participation.getResults()).hasSize(2); + assertThat(participation.getSubmissions()).hasSize(1); + assertThat(participation.getSubmissions().iterator().next().getResults()).hasSize(1); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "USER") + void getDataForTextEditor_asTutor_afterAssessmentDueDate_athenaAndManualResults() throws Exception { + assessmentDueDatePassed(); + + TextSubmission textSubmission = ParticipationFactory.generateTextSubmission("Some text", Language.ENGLISH, true); + textSubmission = textExerciseUtilService.saveTextSubmissionWithAthenaResult(textExercise, textSubmission, TEST_PREFIX + "student1"); + textSubmission = textExerciseUtilService.saveTextSubmissionWithResultAndAssessor(textExercise, textSubmission, TEST_PREFIX + "student1", TEST_PREFIX + "tutor1"); + + StudentParticipation participation = request.get("/api/text-editor/" + textSubmission.getParticipation().getId(), HttpStatus.OK, StudentParticipation.class); + + assertThat(participation.getResults()).hasSize(2); + assertThat(participation.getSubmissions()).hasSize(1); + assertThat(participation.getSubmissions().iterator().next().getResults()).hasSize(1); + } + private void getExampleResultForTutor(HttpStatus expectedStatus, boolean isExample) throws Exception { TextSubmission textSubmission = ParticipationFactory.generateTextSubmission("Some text", Language.ENGLISH, true); textSubmission.setExampleSubmission(isExample); diff --git a/src/test/javascript/spec/component/exercises/shared/headers/header-participation-page.component.spec.ts b/src/test/javascript/spec/component/exercises/shared/headers/header-participation-page.component.spec.ts index be0101124698..70ce4974b5d3 100644 --- a/src/test/javascript/spec/component/exercises/shared/headers/header-participation-page.component.spec.ts +++ b/src/test/javascript/spec/component/exercises/shared/headers/header-participation-page.component.spec.ts @@ -110,4 +110,18 @@ describe('HeaderParticipationPage', () => { component.ngOnChanges(); expect(component.achievedPoints).toBe(42); }); + + it('should select the result with later completion date even if its id is lower', () => { + component.exercise.maxPoints = 100; + const earlierDate = dayjs().subtract(2, 'hours'); + const laterDate = dayjs().subtract(1, 'hours'); + + const resultWithLowerId = { id: 1, score: 80, rated: true, completionDate: laterDate } as Result; + const resultWithHigherId = { id: 2, score: 50, rated: true, completionDate: earlierDate } as Result; + + component.participation = { results: [resultWithHigherId, resultWithLowerId] } as StudentParticipation; + + component.ngOnChanges(); + expect(component.achievedPoints).toBe(80); + }); }); From 40196b2674b49da2d7659292911d91a0760a218f Mon Sep 17 00:00:00 2001 From: Simon Entholzer <33342534+SimonEntholzer@users.noreply.github.com> Date: Tue, 25 Feb 2025 22:01:34 +0100 Subject: [PATCH 15/17] Programming exercises: Improve repository diff report generation performance (#10374) --- .../iris/service/pyris/PyrisDTOService.java | 18 +- .../service/CommitHistoryService.java | 117 ------------- .../programming/service/GitService.java | 4 +- ...ogrammingExerciseGitDiffReportService.java | 161 ++++++++++++------ .../service/RepositoryService.java | 9 +- ...grammingExerciseGitDiffReportResource.java | 22 +-- .../web/repository/RepositoryResource.java | 4 +- 7 files changed, 128 insertions(+), 207 deletions(-) delete mode 100644 src/main/java/de/tum/cit/aet/artemis/programming/service/CommitHistoryService.java diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisDTOService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisDTOService.java index 229e9cf1da5d..34f177ffc313 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisDTOService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisDTOService.java @@ -27,7 +27,7 @@ import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseParticipation; import de.tum.cit.aet.artemis.programming.domain.ProgrammingSubmission; -import de.tum.cit.aet.artemis.programming.domain.Repository; +import de.tum.cit.aet.artemis.programming.domain.VcsRepositoryUri; import de.tum.cit.aet.artemis.programming.service.GitService; import de.tum.cit.aet.artemis.programming.service.RepositoryService; @@ -63,14 +63,7 @@ public PyrisProgrammingExerciseDTO toPyrisProgrammingExerciseDTO(ProgrammingExer // var templateRepositoryContents = new HashMap(); // var solutionRepositoryContents = new HashMap(); - Optional testRepo = Optional.empty(); - try { - testRepo = Optional.ofNullable(gitService.getOrCheckoutRepository(exercise.getVcsTestRepositoryUri(), true)); - } - catch (GitAPIException e) { - log.error("Could not fetch existing test repository", e); - } - var testsRepositoryContents = testRepo.map(repositoryService::getFilesContentFromWorkingCopy).orElse(Map.of()); + Map testsRepositoryContents = getRepositoryContents(exercise.getVcsTestRepositoryUri()); return new PyrisProgrammingExerciseDTO(exercise.getId(), exercise.getTitle(), exercise.getProgrammingLanguage(), templateRepositoryContents, solutionRepositoryContents, testsRepositoryContents, exercise.getProblemStatement(), toInstant(exercise.getReleaseDate()), toInstant(exercise.getDueDate())); @@ -128,7 +121,7 @@ private PyrisResultDTO getLatestResult(ProgrammingSubmission submission) { private Map getFilteredRepositoryContents(ProgrammingExerciseParticipation participation) { var language = participation.getProgrammingExercise().getProgrammingLanguage(); - var repositoryContents = getRepositoryContents(participation); + var repositoryContents = getRepositoryContents(participation.getVcsRepositoryUri()); return repositoryContents.entrySet().stream().filter(entry -> language == null || language.matchesFileExtension(entry.getKey())) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } @@ -138,12 +131,11 @@ private Map getFilteredRepositoryContents(ProgrammingExercisePar * This is an exception safe way to fetch the repository, as it will return an empty map if the repository could not be fetched. * This is useful, as the Pyris call should not fail if the repository is not available. * - * @param participation the participation + * @param repositoryUri the repositoryUri of the repository * @return the repository or empty if it could not be fetched */ - private Map getRepositoryContents(ProgrammingExerciseParticipation participation) { + private Map getRepositoryContents(VcsRepositoryUri repositoryUri) { try { - var repositoryUri = participation.getVcsRepositoryUri(); if (profileService.isLocalVcsActive()) { return Optional.ofNullable(gitService.getBareRepository(repositoryUri)).map(bareRepository -> { var lastCommitObjectId = gitService.getLastCommitHash(repositoryUri); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/CommitHistoryService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/CommitHistoryService.java deleted file mode 100644 index bf4f8496cd93..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/CommitHistoryService.java +++ /dev/null @@ -1,117 +0,0 @@ -package de.tum.cit.aet.artemis.programming.service; - -import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.HashSet; - -import jakarta.validation.constraints.NotNull; - -import org.eclipse.jgit.api.errors.GitAPIException; -import org.eclipse.jgit.diff.DiffFormatter; -import org.eclipse.jgit.diff.RawTextComparator; -import org.eclipse.jgit.revwalk.RevCommit; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Service; - -import de.tum.cit.aet.artemis.core.service.ProfileService; -import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseGitDiffEntry; -import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseGitDiffReport; -import de.tum.cit.aet.artemis.programming.domain.Repository; -import de.tum.cit.aet.artemis.programming.domain.VcsRepositoryUri; - -@Profile(PROFILE_CORE) -@Service -public class CommitHistoryService { - - private static final Logger log = LoggerFactory.getLogger(CommitHistoryService.class); - - private final GitService gitService; - - private final ProfileService profileService; - - private final GitDiffReportParserService gitDiffReportParserService; - - public CommitHistoryService(GitService gitService, ProfileService profileService, GitDiffReportParserService gitDiffReportParserService) { - this.gitService = gitService; - this.profileService = profileService; - this.gitDiffReportParserService = gitDiffReportParserService; - } - - /** - * Creates a new ProgrammingExerciseGitDiffReport containing the git-diff for a repository and two commit hashes. - * - * @param repositoryUri The repository for which the report should be created - * @param commitHash1 The first commit hash - * @param commitHash2 The second commit hash - * @return The report with the changes between the two commits - * @throws GitAPIException If an error occurs while accessing the git repository - * @throws IOException If an error occurs while accessing the file system - */ - public ProgrammingExerciseGitDiffReport generateReportForCommits(VcsRepositoryUri repositoryUri, String commitHash1, String commitHash2) throws GitAPIException, IOException { - - Repository repository; - if (profileService.isLocalVcsActive()) { - log.debug("Using local VCS generateReportForCommits on repo {}", repositoryUri); - repository = gitService.getBareRepository(repositoryUri); - } - else { - log.debug("Checking out repo {} for generateReportForCommits", repositoryUri); - repository = gitService.getOrCheckoutRepository(repositoryUri, true); - } - - RevCommit commitOld = repository.parseCommit(repository.resolve(commitHash1)); - RevCommit commitNew = repository.parseCommit(repository.resolve(commitHash2)); - - if (commitOld == null || commitNew == null) { - log.error("Could not find the commits with the provided commit hashes in the repository: {} and {}", commitHash1, commitHash2); - return null; - } - - return createReport(repository, commitOld, commitNew); - } - - /** - * Creates a new ProgrammingExerciseGitDiffReport containing the git-diff for a participation and a commit hash. - * If both commit hashes are the same, the report will be the diff of the commit with an empty file. - * - * @param repository The repository for which the report should be created - * @param commitOld The old commit - * @param commitNew The new commit - * @return The report with the changes between the two commits - * @throws IOException If an error occurs while accessing the file system - */ - @NotNull - private ProgrammingExerciseGitDiffReport createReport(Repository repository, RevCommit commitOld, RevCommit commitNew) throws IOException { - StringBuilder diffs = new StringBuilder(); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - try (DiffFormatter formatter = new DiffFormatter(out)) { - formatter.setRepository(repository); - formatter.setContext(10); // Set the context lines - formatter.setDiffComparator(RawTextComparator.DEFAULT); - formatter.setDetectRenames(true); - - // If the commit hashes are the same, we want to compare the commit with an empty file - // This is useful for the initial commit of a repository - if (commitOld.getName().equals(commitNew.getName())) { - formatter.format(null, commitNew); - } - else { - formatter.format(commitOld, commitNew); - } - - diffs.append(out.toString(StandardCharsets.UTF_8)); - } - var programmingExerciseGitDiffEntries = gitDiffReportParserService.extractDiffEntries(diffs.toString(), false, false); - var report = new ProgrammingExerciseGitDiffReport(); - for (ProgrammingExerciseGitDiffEntry gitDiffEntry : programmingExerciseGitDiffEntries) { - gitDiffEntry.setGitDiffReport(report); - } - report.setEntries(new HashSet<>(programmingExerciseGitDiffEntries)); - return report; - } -} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/GitService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/GitService.java index ac7ab597fb5a..e5c6660dab43 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/GitService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/GitService.java @@ -343,7 +343,7 @@ public Repository getOrCheckoutRepository(VcsRepositoryUri repoUri, Path localPa * @throws GitException if the same repository is attempted to be cloned multiple times. * @throws InvalidPathException if the repository could not be checked out Because it contains unmappable characters. */ - public Repository getOrCheckoutRepository(VcsRepositoryUri sourceRepoUri, VcsRepositoryUri targetRepoUri, Path localPath, boolean pullOnGet) + private Repository getOrCheckoutRepository(VcsRepositoryUri sourceRepoUri, VcsRepositoryUri targetRepoUri, Path localPath, boolean pullOnGet) throws GitAPIException, GitException, InvalidPathException { return getOrCheckoutRepository(sourceRepoUri, targetRepoUri, localPath, pullOnGet, defaultBranch); } @@ -1108,7 +1108,7 @@ public Optional getFileByName(Repository repo, String filename) { * @return True if the status is clean * @throws GitAPIException if the state of the repository could not be retrieved. */ - public boolean isClean(Repository repo) throws GitAPIException { + public boolean isWorkingCopyClean(Repository repo) throws GitAPIException { try (Git git = new Git(repo)) { Status status = git.status().call(); return status.isClean(); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseGitDiffReportService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseGitDiffReportService.java index 785d200ad349..b4be55eac4d5 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseGitDiffReportService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseGitDiffReportService.java @@ -4,6 +4,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.ArrayList; import java.util.Comparator; @@ -13,24 +14,24 @@ import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.diff.DiffFormatter; +import org.eclipse.jgit.diff.RawTextComparator; +import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.treewalk.FileTreeIterator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; -import org.springframework.util.FileSystemUtils; import de.tum.cit.aet.artemis.core.domain.DomainObject; import de.tum.cit.aet.artemis.core.exception.InternalServerErrorException; -import de.tum.cit.aet.artemis.core.service.FileService; +import de.tum.cit.aet.artemis.core.service.ProfileService; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseGitDiffEntry; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseGitDiffReport; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseParticipation; import de.tum.cit.aet.artemis.programming.domain.ProgrammingSubmission; import de.tum.cit.aet.artemis.programming.domain.Repository; -import de.tum.cit.aet.artemis.programming.domain.SolutionProgrammingExerciseParticipation; -import de.tum.cit.aet.artemis.programming.domain.TemplateProgrammingExerciseParticipation; import de.tum.cit.aet.artemis.programming.domain.VcsRepositoryUri; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseGitDiffReportRepository; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseRepository; @@ -59,23 +60,23 @@ public class ProgrammingExerciseGitDiffReportService { private final SolutionProgrammingExerciseParticipationRepository solutionProgrammingExerciseParticipationRepository; - private final FileService fileService; - private final GitDiffReportParserService gitDiffReportParserService; + private final ProfileService profileService; + public ProgrammingExerciseGitDiffReportService(GitService gitService, ProgrammingExerciseGitDiffReportRepository programmingExerciseGitDiffReportRepository, ProgrammingSubmissionRepository programmingSubmissionRepository, ProgrammingExerciseRepository programmingExerciseRepository, TemplateProgrammingExerciseParticipationRepository templateProgrammingExerciseParticipationRepository, - SolutionProgrammingExerciseParticipationRepository solutionProgrammingExerciseParticipationRepository, FileService fileService, - GitDiffReportParserService gitDiffReportParserService) { + SolutionProgrammingExerciseParticipationRepository solutionProgrammingExerciseParticipationRepository, GitDiffReportParserService gitDiffReportParserService, + ProfileService profileService) { this.gitService = gitService; this.programmingExerciseGitDiffReportRepository = programmingExerciseGitDiffReportRepository; this.programmingSubmissionRepository = programmingSubmissionRepository; this.programmingExerciseRepository = programmingExerciseRepository; this.templateProgrammingExerciseParticipationRepository = templateProgrammingExerciseParticipationRepository; this.solutionProgrammingExerciseParticipationRepository = solutionProgrammingExerciseParticipationRepository; - this.fileService = fileService; this.gitDiffReportParserService = gitDiffReportParserService; + this.profileService = profileService; } /** @@ -113,7 +114,7 @@ public ProgrammingExerciseGitDiffReport updateReport(ProgrammingExercise program } try { - var newReport = generateReport(templateParticipation, solutionParticipation); + var newReport = createReportForTemplateWithSolution(templateParticipation.getVcsRepositoryUri(), solutionParticipation.getVcsRepositoryUri()); newReport.setTemplateRepositoryCommitHash(templateHash); newReport.setSolutionRepositoryCommitHash(solutionHash); newReport.setProgrammingExercise(programmingExercise); @@ -183,14 +184,17 @@ public ProgrammingExerciseGitDiffReport getOrCreateReportOfExercise(ProgrammingE public ProgrammingExerciseGitDiffReport createReportForSubmissionWithTemplate(ProgrammingExercise exercise, ProgrammingSubmission submission) throws GitAPIException, IOException { var templateParticipation = templateProgrammingExerciseParticipationRepository.findByProgrammingExerciseId(exercise.getId()).orElseThrow(); - Repository templateRepo = prepareTemplateRepository(templateParticipation); + var vcsRepositoryUri = ((ProgrammingExerciseParticipation) submission.getParticipation()).getVcsRepositoryUri(); + + // TODO: for LocalVC, we should do that without checking out the repository + // TODO: find a way to make report creation work with gitService.getBareRepository(vcsRepositoryUri); + Repository templateRepo = prepareRepository(templateParticipation.getVcsRepositoryUri()); + Repository submissionRepository = gitService.checkoutRepositoryAtCommit(vcsRepositoryUri, submission.getCommitHash(), false); - var repo1 = gitService.checkoutRepositoryAtCommit(((ProgrammingExerciseParticipation) submission.getParticipation()).getVcsRepositoryUri(), submission.getCommitHash(), - false); var oldTreeParser = new FileTreeIterator(templateRepo); - var newTreeParser = new FileTreeIterator(repo1); + var newTreeParser = new FileTreeIterator(submissionRepository); var report = createReport(templateRepo, oldTreeParser, newTreeParser); - gitService.switchBackToDefaultBranchHead(repo1); + gitService.switchBackToDefaultBranchHead(submissionRepository); return report; } @@ -225,18 +229,19 @@ public int calculateNumberOfDiffLinesBetweenRepos(VcsRepositoryUri urlRepoA, Pat * Creates a new ProgrammingExerciseGitDiffReport for an exercise. * It will take the git-diff between the template and solution repositories and return all changes. * - * @param templateParticipation The participation for the template - * @param solutionParticipation The participation for the solution + * @param templateVcsRepositoryUri The vcsRepositoryUri of the template participation + * @param solutionVcsRepositoryUri The vcsRepositoryUri of the solution participation * @return The changes between template and solution * @throws GitAPIException If there was an issue with JGit */ - private ProgrammingExerciseGitDiffReport generateReport(TemplateProgrammingExerciseParticipation templateParticipation, - SolutionProgrammingExerciseParticipation solutionParticipation) throws GitAPIException, IOException { - // TODO: in case of LocalVC, we should calculate the diff in the bare origin repository - Repository templateRepo = prepareTemplateRepository(templateParticipation); - var solutionRepo = gitService.getOrCheckoutRepository(solutionParticipation.getVcsRepositoryUri(), true); - gitService.resetToOriginHead(solutionRepo); - gitService.pullIgnoreConflicts(solutionRepo); + private ProgrammingExerciseGitDiffReport createReportForTemplateWithSolution(VcsRepositoryUri templateVcsRepositoryUri, VcsRepositoryUri solutionVcsRepositoryUri) + throws GitAPIException, IOException { + Repository templateRepo; + Repository solutionRepo; + // TODO: for LocalVC, we should do that without checking out the repository + // TODO: find a way to make report creation work with gitService.getBareRepository(vcsRepositoryUri); + templateRepo = prepareRepository(templateVcsRepositoryUri); + solutionRepo = prepareRepository(solutionVcsRepositoryUri); var oldTreeParser = new FileTreeIterator(templateRepo); var newTreeParser = new FileTreeIterator(solutionRepo); @@ -245,14 +250,14 @@ private ProgrammingExerciseGitDiffReport generateReport(TemplateProgrammingExerc } /** - * Prepares the template repository for the git diff calculation by checking it out and resetting it to the origin head. + * Prepares a repository for the git diff calculation by checking it out and resetting it to the origin head. * - * @param templateParticipation The participation for the template + * @param vcsRepositoryUri The vcsRepositoryUri of the participation of the repository * @return The checked out template repository * @throws GitAPIException If an error occurs while accessing the git repository */ - private Repository prepareTemplateRepository(TemplateProgrammingExerciseParticipation templateParticipation) throws GitAPIException { - var templateRepo = gitService.getOrCheckoutRepository(templateParticipation.getVcsRepositoryUri(), true); + private Repository prepareRepository(VcsRepositoryUri vcsRepositoryUri) throws GitAPIException { + Repository templateRepo = gitService.getOrCheckoutRepository(vcsRepositoryUri, true); gitService.resetToOriginHead(templateRepo); gitService.pullIgnoreConflicts(templateRepo); return templateRepo; @@ -269,34 +274,7 @@ private Repository prepareTemplateRepository(TemplateProgrammingExerciseParticip */ public ProgrammingExerciseGitDiffReport generateReportForSubmissions(ProgrammingSubmission submission1, ProgrammingSubmission submission2) throws GitAPIException, IOException { var repositoryUri = ((ProgrammingExerciseParticipation) submission1.getParticipation()).getVcsRepositoryUri(); - var repo1 = gitService.getOrCheckoutRepository(repositoryUri, true); - var repo1Path = repo1.getLocalPath(); - var repo2Path = fileService.getTemporaryUniqueSubfolderPath(repo1Path.getParent(), 5); - FileSystemUtils.copyRecursively(repo1Path, repo2Path); - repo1 = gitService.checkoutRepositoryAtCommit(repo1, submission1.getCommitHash()); - var repo2 = gitService.getExistingCheckedOutRepositoryByLocalPath(repo2Path, repositoryUri); - repo2 = gitService.checkoutRepositoryAtCommit(repo2, submission2.getCommitHash()); - return parseFilesAndCreateReport(repo1, repo2); - } - - /** - * Parses the files of the given repositories and creates a new ProgrammingExerciseGitDiffReport containing the git-diff. - * - * @param repo1 The first repository - * @param repo2 The second repository - * @return The report with the changes between the two repositories at their checked out state - * @throws IOException If an error occurs while accessing the file system - * @throws GitAPIException If an error occurs while accessing the git repository - */ - @NotNull - private ProgrammingExerciseGitDiffReport parseFilesAndCreateReport(Repository repo1, Repository repo2) throws IOException, GitAPIException { - var oldTreeParser = new FileTreeIterator(repo1); - var newTreeParser = new FileTreeIterator(repo2); - - var report = createReport(repo1, oldTreeParser, newTreeParser); - gitService.switchBackToDefaultBranchHead(repo1); - gitService.switchBackToDefaultBranchHead(repo2); - return report; + return generateReportForCommits(repositoryUri, submission1.getCommitHash(), submission2.getCommitHash()); } /** @@ -332,4 +310,75 @@ private boolean canUseExistingReport(ProgrammingExerciseGitDiffReport report, St return report.getTemplateRepositoryCommitHash().equals(templateHash) && report.getSolutionRepositoryCommitHash().equals(solutionHash); } + /** + * Creates a new ProgrammingExerciseGitDiffReport containing the git-diff for a repository and two commit hashes. + * + * @param repositoryUri The repository for which the report should be created + * @param commitHash1 The first commit hash + * @param commitHash2 The second commit hash + * @return The report with the changes between the two commits + * @throws GitAPIException If an error occurs while accessing the git repository + * @throws IOException If an error occurs while accessing the file system + */ + public ProgrammingExerciseGitDiffReport generateReportForCommits(VcsRepositoryUri repositoryUri, String commitHash1, String commitHash2) throws GitAPIException, IOException { + Repository repository; + if (profileService.isLocalVcsActive()) { + log.debug("Using local VCS generateReportForCommits on repo {}", repositoryUri); + repository = gitService.getBareRepository(repositoryUri); + } + else { + log.debug("Checking out repo {} for generateReportForCommits", repositoryUri); + repository = gitService.getOrCheckoutRepository(repositoryUri, true); + } + + RevCommit commitOld = repository.parseCommit(repository.resolve(commitHash1)); + RevCommit commitNew = repository.parseCommit(repository.resolve(commitHash2)); + + if (commitOld == null || commitNew == null) { + log.error("Could not find the commits with the provided commit hashes in the repository: {} and {}", commitHash1, commitHash2); + return null; + } + + return createReport(repository, commitOld, commitNew); + } + + /** + * Creates a new ProgrammingExerciseGitDiffReport containing the git-diff for a participation and a commit hash. + * If both commit hashes are the same, the report will be the diff of the commit with an empty file. + * + * @param repository The repository for which the report should be created + * @param commitOld The old commit + * @param commitNew The new commit + * @return The report with the changes between the two commits + * @throws IOException If an error occurs while accessing the file system + */ + @NotNull + private ProgrammingExerciseGitDiffReport createReport(Repository repository, RevCommit commitOld, RevCommit commitNew) throws IOException { + StringBuilder diffs = new StringBuilder(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (DiffFormatter formatter = new DiffFormatter(out)) { + formatter.setRepository(repository); + formatter.setContext(10); // Set the context lines + formatter.setDiffComparator(RawTextComparator.DEFAULT); + formatter.setDetectRenames(true); + + // If the commit hashes are the same, we want to compare the commit with an empty file + // This is useful for the initial commit of a repository + if (commitOld.getName().equals(commitNew.getName())) { + formatter.format(null, commitNew); + } + else { + formatter.format(commitOld, commitNew); + } + + diffs.append(out.toString(StandardCharsets.UTF_8)); + } + var programmingExerciseGitDiffEntries = gitDiffReportParserService.extractDiffEntries(diffs.toString(), false, false); + var report = new ProgrammingExerciseGitDiffReport(); + for (ProgrammingExerciseGitDiffEntry gitDiffEntry : programmingExerciseGitDiffEntries) { + gitDiffEntry.setGitDiffReport(report); + } + report.setEntries(new HashSet<>(programmingExerciseGitDiffEntries)); + return report; + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/RepositoryService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/RepositoryService.java index 52b1a718faf7..31d7900d1d99 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/RepositoryService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/RepositoryService.java @@ -189,6 +189,7 @@ public Map getFilesContentFromWorkingCopy(Repository repository) * @throws IOException If an I/O error occurs during the file content retrieval process, including issues with * opening and reading the file stream. */ + // TODO: can we offer this method without commitId as well? public Map getFilesContentFromBareRepository(Repository repository, String commitId) throws IOException { RevWalk revWalk = new RevWalk(repository); RevCommit commit = revWalk.parseCommit(repository.resolve(commitId)); @@ -482,9 +483,9 @@ public Optional savePreliminaryCodeEditorAccessLog(Repository repo * @return a dto to determine the status of the repository. * @throws GitAPIException if the repository status can't be retrieved. */ - public boolean isClean(VcsRepositoryUri repositoryUri) throws GitAPIException { + public boolean isWorkingCopyClean(VcsRepositoryUri repositoryUri) throws GitAPIException { Repository repository = gitService.getOrCheckoutRepository(repositoryUri, true); - return gitService.isClean(repository); + return gitService.isWorkingCopyClean(repository); } /** @@ -495,9 +496,9 @@ public boolean isClean(VcsRepositoryUri repositoryUri) throws GitAPIException { * @return a dto to determine the status of the repository. * @throws GitAPIException if the repository status can't be retrieved. */ - public boolean isClean(VcsRepositoryUri repositoryUri, String defaultBranch) throws GitAPIException { + public boolean isWorkingCopyClean(VcsRepositoryUri repositoryUri, String defaultBranch) throws GitAPIException { Repository repository = gitService.getOrCheckoutRepository(repositoryUri, true, defaultBranch); - return gitService.isClean(repository); + return gitService.isWorkingCopyClean(repository); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseGitDiffReportResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseGitDiffReportResource.java index b561b6b331bb..d4823b769a53 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseGitDiffReportResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseGitDiffReportResource.java @@ -31,7 +31,6 @@ import de.tum.cit.aet.artemis.programming.dto.ProgrammingExerciseGitDiffReportDTO; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseRepository; import de.tum.cit.aet.artemis.programming.repository.ProgrammingSubmissionRepository; -import de.tum.cit.aet.artemis.programming.service.CommitHistoryService; import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseGitDiffReportService; import de.tum.cit.aet.artemis.programming.service.RepositoryService; @@ -57,22 +56,19 @@ public class ProgrammingExerciseGitDiffReportResource { private final ParticipationAuthorizationCheckService participationAuthCheckService; - private final CommitHistoryService commitHistoryService; - private final RepositoryService repositoryService; private static final String ENTITY_NAME = "programmingExerciseGitDiffReportEntry"; public ProgrammingExerciseGitDiffReportResource(AuthorizationCheckService authCheckService, ProgrammingExerciseRepository programmingExerciseRepository, ParticipationRepository participationRepository, ProgrammingExerciseGitDiffReportService gitDiffReportService, ProgrammingSubmissionRepository submissionRepository, - ParticipationAuthorizationCheckService participationAuthCheckService, CommitHistoryService commitHistoryService, RepositoryService repositoryService) { + ParticipationAuthorizationCheckService participationAuthCheckService, RepositoryService repositoryService) { this.authCheckService = authCheckService; this.programmingExerciseRepository = programmingExerciseRepository; this.participationRepository = participationRepository; this.gitDiffReportService = gitDiffReportService; this.submissionRepository = submissionRepository; this.participationAuthCheckService = participationAuthCheckService; - this.commitHistoryService = commitHistoryService; this.repositoryService = repositoryService; } @@ -126,24 +122,24 @@ public ResponseEntity getGitDiffReportForSu } /** - * GET exercises/:exerciseId/submissions/:submissionId1/diff-report-with-template : Get the diff report for a submission of a programming exercise with the template of the + * GET exercises/:exerciseId/submissions/:submissionId/diff-report-with-template : Get the diff report for a submission of a programming exercise with the template of the * exercise. * The current user needs to have at least instructor access to the exercise to fetch the diff report with the template. * - * @param exerciseId the id of the exercise the submission and the template belong to - * @param submissionId1 the id of the submission + * @param exerciseId the id of the exercise the submission and the template belong to + * @param submissionId the id of the submission * @return the {@link ResponseEntity} with status {@code 200 (Ok)} and with the diff report as body * @throws GitAPIException if errors occur while accessing the git repository * @throws IOException if errors occur while accessing the file system */ - @GetMapping("programming-exercises/{exerciseId}/submissions/{submissionId1}/diff-report-with-template") + @GetMapping("programming-exercises/{exerciseId}/submissions/{submissionId}/diff-report-with-template") @EnforceAtLeastInstructor - public ResponseEntity getGitDiffReportForSubmissionWithTemplate(@PathVariable long exerciseId, @PathVariable long submissionId1) + public ResponseEntity getGitDiffReportForSubmissionWithTemplate(@PathVariable long exerciseId, @PathVariable long submissionId) throws GitAPIException, IOException { - log.debug("REST request to get a ProgrammingExerciseGitDiffReport for submission {} with the template of exercise {}", submissionId1, exerciseId); + log.debug("REST request to get a ProgrammingExerciseGitDiffReport for submission {} with the template of exercise {}", submissionId, exerciseId); var exercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.INSTRUCTOR, exercise, null); - var submission = submissionRepository.findById(submissionId1).orElseThrow(); + var submission = submissionRepository.findById(submissionId).orElseThrow(); if (!submission.getParticipation().getExercise().getId().equals(exerciseId)) { throw new IllegalArgumentException("The submission does not belong to the exercise"); } @@ -193,7 +189,7 @@ else if (repositoryType != null) { else { throw new BadRequestAlertException("Either participationId or repositoryType must be provided", ENTITY_NAME, "missingParameters"); } - var report = commitHistoryService.generateReportForCommits(repositoryUri, commitHash1, commitHash2); + var report = gitDiffReportService.generateReportForCommits(repositoryUri, commitHash1, commitHash2); return ResponseEntity.ok(new ProgrammingExerciseGitDiffReportDTO(report)); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/RepositoryResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/RepositoryResource.java index abe9b9ec40e2..9e8a744177c9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/RepositoryResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/RepositoryResource.java @@ -330,11 +330,11 @@ public ResponseEntity getStatus(Long domainId) throws GitAP // This check reduces the amount of REST-calls that retrieve the default branch of a repository. // Retrieving the default branch is not necessary if the repository is already cached. if (gitService.isRepositoryCached(repositoryUri)) { - isClean = repositoryService.isClean(repositoryUri); + isClean = repositoryService.isWorkingCopyClean(repositoryUri); } else { String branch = getOrRetrieveBranchOfDomainObject(domainId); - isClean = repositoryService.isClean(repositoryUri, branch); + isClean = repositoryService.isWorkingCopyClean(repositoryUri, branch); } repositoryStatus = isClean ? CLEAN : UNCOMMITTED_CHANGES; } From ed100700aa0d0d783dcdebcd11e8f8e3601d59ae Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Tue, 25 Feb 2025 22:39:19 +0100 Subject: [PATCH 16/17] Development: Fix client tests --- .../component/competencies/edit-competency.component.spec.ts | 5 +++-- .../competencies/edit-prerequisite.component.spec.ts | 5 +++-- .../theia/programming-exercise-theia.component.spec.ts | 5 +++-- .../filter/standardized-competency-filter.component.spec.ts | 4 +++- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/test/javascript/spec/component/competencies/edit-competency.component.spec.ts b/src/test/javascript/spec/component/competencies/edit-competency.component.spec.ts index 44decafa9b97..cee0853b96a3 100644 --- a/src/test/javascript/spec/component/competencies/edit-competency.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/edit-competency.component.spec.ts @@ -1,6 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { AlertService } from 'app/core/util/alert.service'; import { ActivatedRoute, Router } from '@angular/router'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; import { of } from 'rxjs'; import { HttpResponse } from '@angular/common/http'; import { By } from '@angular/platform-browser'; @@ -14,14 +15,14 @@ import { MockRouter } from '../../helpers/mocks/mock-router'; import { CompetencyFormComponent } from 'app/course/competencies/forms/competency/competency-form.component'; import { OwlNativeDateTimeModule } from '@danielmoncada/angular-datetime-picker'; import { MockResizeObserver } from '../../helpers/mocks/service/mock-resize-observer'; -import { MockComponent, MockModule, MockProvider } from 'ng-mocks'; +import { MockComponent, MockDirective, MockModule, MockProvider } from 'ng-mocks'; describe('EditCompetencyComponent', () => { let editCompetencyComponentFixture: ComponentFixture; let editCompetencyComponent: EditCompetencyComponent; beforeEach(() => { TestBed.configureTestingModule({ - imports: [EditCompetencyComponent, MockModule(OwlNativeDateTimeModule), MockComponent(CompetencyFormComponent)], + imports: [EditCompetencyComponent, MockModule(OwlNativeDateTimeModule), MockComponent(CompetencyFormComponent), MockDirective(TranslateDirective)], providers: [ MockProvider(LectureService), MockProvider(CompetencyService), diff --git a/src/test/javascript/spec/component/competencies/edit-prerequisite.component.spec.ts b/src/test/javascript/spec/component/competencies/edit-prerequisite.component.spec.ts index bd18e711ac6f..17d1a4047409 100644 --- a/src/test/javascript/spec/component/competencies/edit-prerequisite.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/edit-prerequisite.component.spec.ts @@ -1,5 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MockComponent, MockModule, MockProvider } from 'ng-mocks'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; +import { MockComponent, MockDirective, MockModule, MockProvider } from 'ng-mocks'; import { AlertService } from 'app/core/util/alert.service'; import { ActivatedRoute, Router } from '@angular/router'; import { of } from 'rxjs'; @@ -22,7 +23,7 @@ describe('EditPrerequisiteComponent', () => { let editPrerequisiteComponent: EditPrerequisiteComponent; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [EditPrerequisiteComponent, MockModule(OwlNativeDateTimeModule), MockComponent(PrerequisiteFormComponent)], + imports: [EditPrerequisiteComponent, MockModule(OwlNativeDateTimeModule), MockComponent(PrerequisiteFormComponent), MockDirective(TranslateDirective)], providers: [ MockProvider(LectureService), MockProvider(PrerequisiteService), diff --git a/src/test/javascript/spec/component/programming-exercise/update-components/theia/programming-exercise-theia.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/update-components/theia/programming-exercise-theia.component.spec.ts index 132053b601a3..af1029a6ad39 100644 --- a/src/test/javascript/spec/component/programming-exercise/update-components/theia/programming-exercise-theia.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/update-components/theia/programming-exercise-theia.component.spec.ts @@ -1,5 +1,6 @@ import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; -import { MockPipe } from 'ng-mocks'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; +import { MockDirective, MockPipe } from 'ng-mocks'; import { ActivatedRoute } from '@angular/router'; import { of } from 'rxjs'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; @@ -21,7 +22,7 @@ describe('ProgrammingExerciseTheiaComponent', () => { }; TestBed.configureTestingModule({ imports: [ProgrammingExerciseTheiaComponent], - declarations: [MockPipe(ArtemisTranslatePipe), MockPipe(RemoveKeysPipe)], + declarations: [MockPipe(ArtemisTranslatePipe), MockPipe(RemoveKeysPipe), MockDirective(TranslateDirective)], providers: [ { provide: ActivatedRoute, diff --git a/src/test/javascript/spec/component/standardized-competencies/filter/standardized-competency-filter.component.spec.ts b/src/test/javascript/spec/component/standardized-competencies/filter/standardized-competency-filter.component.spec.ts index 3a40a9182124..3ddb8c0b29c5 100644 --- a/src/test/javascript/spec/component/standardized-competencies/filter/standardized-competency-filter.component.spec.ts +++ b/src/test/javascript/spec/component/standardized-competencies/filter/standardized-competency-filter.component.spec.ts @@ -1,6 +1,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormsModule } from '@angular/forms'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; import { StandardizedCompetencyFilterComponent } from 'app/shared/standardized-competencies/standardized-competency-filter.component'; +import { MockDirective } from 'ng-mocks'; describe('StandardizedCompetencyFilterComponent', () => { let componentFixture: ComponentFixture; @@ -8,7 +10,7 @@ describe('StandardizedCompetencyFilterComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [StandardizedCompetencyFilterComponent, FormsModule], + imports: [StandardizedCompetencyFilterComponent, FormsModule, MockDirective(TranslateDirective)], declarations: [], providers: [], }) From fede0c31c29346a08f1a9369a7df34c487020c59 Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Wed, 26 Feb 2025 09:33:12 +0100 Subject: [PATCH 17/17] Development: Workaround https://github.com/spring-projects/spring-data-jpa/issues/3789 --- build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 68661dd6d076..54bf35dbd085 100644 --- a/build.gradle +++ b/build.gradle @@ -292,7 +292,8 @@ dependencies { implementation "org.springframework.boot:spring-boot-starter-logging:${spring_boot_version}" implementation "org.springframework.boot:spring-boot-starter-actuator:${spring_boot_version}" implementation "org.springframework.boot:spring-boot-starter-aop:${spring_boot_version}" - implementation "org.springframework.boot:spring-boot-starter-data-jpa:${spring_boot_version}" + implementation "org.springframework.boot:spring-boot-starter-data-jpa:3.4.2" + implementation "org.springframework.data:spring-data-jpa:3.4.2" implementation "org.springframework.boot:spring-boot-starter-security:${spring_boot_version}" implementation "org.springframework.boot:spring-boot-starter-web:${spring_boot_version}" implementation "org.springframework.boot:spring-boot-starter-tomcat:${spring_boot_version}"