From 32212f8b9639a9e17bfacf1486a4c04bdab2168f Mon Sep 17 00:00:00 2001 From: Tim Cremer <65229601+cremertim@users.noreply.github.com> Date: Thu, 30 Jan 2025 16:28:52 +0100 Subject: [PATCH 1/8] Communication: Add Artemis intelligence rewrite action for FAQs (#10157) --- .../iris/service/IrisRewritingService.java | 97 +++++++++++++++++++ .../iris/service/pyris/PyrisDTOService.java | 4 + .../pyris/PyrisStatusUpdateService.java | 20 +++- ...petencyExtractionPipelineExecutionDTO.java | 2 +- .../dto/data/PyrisRewriteTextRequestDTO.java | 9 ++ .../PyrisRewritingPipelineExecutionDTO.java | 15 +++ .../PyrisRewritingStatusUpdateDTO.java | 21 ++++ .../pyris/dto/rewriting/RewritingVariant.java | 5 + .../iris/service/pyris/job/RewritingJob.java | 22 +++++ .../iris/web/IrisRewritingResource.java | 66 +++++++++++++ .../open/PublicPyrisStatusUpdateResource.java | 27 ++++++ .../webapp/app/faq/faq-update.component.html | 3 +- .../webapp/app/faq/faq-update.component.ts | 29 ++++-- src/main/webapp/app/icons/icons.ts | 13 +++ .../markdown-editor-monaco.component.html | 24 +++++ .../markdown-editor-monaco.component.ts | 15 ++- .../adapter/monaco-text-editor.adapter.ts | 7 ++ .../actions/adapter/text-editor.interface.ts | 6 ++ .../artemis-intelligence.service.ts | 66 +++++++++++++ .../artemis-intelligence/rewrite.action.ts | 29 ++++++ .../artemis-intelligence/rewriting-variant.ts | 6 ++ .../model/actions/text-editor-action.model.ts | 20 ++++ src/main/webapp/i18n/de/markdownEditor.json | 13 ++- src/main/webapp/i18n/en/markdownEditor.json | 13 ++- .../connector/IrisRequestMockProvider.java | 15 +++ .../IrisCodeStyleArchitectureTest.java | 2 +- .../faq/faq-update.component.spec.ts | 13 +++ ...postings-markdown-editor.component.spec.ts | 1 + .../artemis-intelligence.service.spec.ts | 75 ++++++++++++++ 29 files changed, 622 insertions(+), 16 deletions(-) create mode 100644 src/main/java/de/tum/cit/aet/artemis/iris/service/IrisRewritingService.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/data/PyrisRewriteTextRequestDTO.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/rewriting/PyrisRewritingPipelineExecutionDTO.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/rewriting/PyrisRewritingStatusUpdateDTO.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/rewriting/RewritingVariant.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/job/RewritingJob.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/iris/web/IrisRewritingResource.java create mode 100644 src/main/webapp/app/shared/monaco-editor/model/actions/artemis-intelligence/artemis-intelligence.service.ts create mode 100644 src/main/webapp/app/shared/monaco-editor/model/actions/artemis-intelligence/rewrite.action.ts create mode 100644 src/main/webapp/app/shared/monaco-editor/model/actions/artemis-intelligence/rewriting-variant.ts create mode 100644 src/test/javascript/spec/service/artemis-intelligence.service.spec.ts diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisRewritingService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisRewritingService.java new file mode 100644 index 000000000000..36432e696de9 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisRewritingService.java @@ -0,0 +1,97 @@ +package de.tum.cit.aet.artemis.iris.service; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_IRIS; + +import java.util.Optional; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import de.tum.cit.aet.artemis.core.domain.Course; +import de.tum.cit.aet.artemis.core.domain.LLMServiceType; +import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.core.repository.CourseRepository; +import de.tum.cit.aet.artemis.core.repository.UserRepository; +import de.tum.cit.aet.artemis.core.service.LLMTokenUsageService; +import de.tum.cit.aet.artemis.iris.service.pyris.PyrisJobService; +import de.tum.cit.aet.artemis.iris.service.pyris.PyrisPipelineService; +import de.tum.cit.aet.artemis.iris.service.pyris.dto.rewriting.PyrisRewritingPipelineExecutionDTO; +import de.tum.cit.aet.artemis.iris.service.pyris.dto.rewriting.PyrisRewritingStatusUpdateDTO; +import de.tum.cit.aet.artemis.iris.service.pyris.dto.rewriting.RewritingVariant; +import de.tum.cit.aet.artemis.iris.service.pyris.job.RewritingJob; +import de.tum.cit.aet.artemis.iris.service.websocket.IrisWebsocketService; + +/** + * Service to handle the rewriting subsystem of Iris. + */ +@Service +@Profile(PROFILE_IRIS) +public class IrisRewritingService { + + private final PyrisPipelineService pyrisPipelineService; + + private final LLMTokenUsageService llmTokenUsageService; + + private final CourseRepository courseRepository; + + private final IrisWebsocketService websocketService; + + private final PyrisJobService pyrisJobService; + + private final UserRepository userRepository; + + public IrisRewritingService(PyrisPipelineService pyrisPipelineService, LLMTokenUsageService llmTokenUsageService, CourseRepository courseRepository, + IrisWebsocketService websocketService, PyrisJobService pyrisJobService, UserRepository userRepository) { + this.pyrisPipelineService = pyrisPipelineService; + this.llmTokenUsageService = llmTokenUsageService; + this.courseRepository = courseRepository; + this.websocketService = websocketService; + this.pyrisJobService = pyrisJobService; + this.userRepository = userRepository; + } + + /** + * Executes the rewriting pipeline on Pyris + * + * @param user the user for which the pipeline should be executed + * @param course the course for which the pipeline should be executed + * @param variant the rewriting variant to be used + * @param toBeRewritten the text to be rewritten + */ + public void executeRewritingPipeline(User user, Course course, RewritingVariant variant, String toBeRewritten) { + // @formatter:off + pyrisPipelineService.executePipeline( + "rewriting", + variant.toString(), + Optional.empty(), + pyrisJobService.createTokenForJob(token -> new RewritingJob(token, course.getId(), user.getId())), + executionDto -> new PyrisRewritingPipelineExecutionDTO(executionDto, toBeRewritten), + stages -> websocketService.send(user.getLogin(), websocketTopic(course.getId()), new PyrisRewritingStatusUpdateDTO(stages, null, null)) + ); + // @formatter:on + } + + /** + * Takes a status update from Pyris containing a new rewriting result and sends it to the client via websocket + * + * @param job Job related to the status update + * @param statusUpdate the status update containing text recommendations + * @return the same job that was passed in + */ + public RewritingJob handleStatusUpdate(RewritingJob job, PyrisRewritingStatusUpdateDTO statusUpdate) { + Course course = courseRepository.findByIdForUpdateElseThrow(job.courseId()); + if (statusUpdate.tokens() != null && !statusUpdate.tokens().isEmpty()) { + llmTokenUsageService.saveLLMTokenUsage(statusUpdate.tokens(), LLMServiceType.IRIS, builder -> builder.withCourse(course.getId()).withUser(job.userId())); + } + + var user = userRepository.findById(job.userId()).orElseThrow(); + websocketService.send(user.getLogin(), websocketTopic(job.courseId()), statusUpdate); + + return job; + } + + private static String websocketTopic(long courseId) { + return "rewriting/" + courseId; + } + +} 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 ca42f85363bc..229e9cf1da5d 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 @@ -59,6 +59,10 @@ public PyrisDTOService(GitService gitService, RepositoryService repositoryServic public PyrisProgrammingExerciseDTO toPyrisProgrammingExerciseDTO(ProgrammingExercise exercise) { var templateRepositoryContents = getFilteredRepositoryContents(exercise.getTemplateParticipation()); var solutionRepositoryContents = getFilteredRepositoryContents(exercise.getSolutionParticipation()); + + // var templateRepositoryContents = new HashMap(); + // var solutionRepositoryContents = new HashMap(); + Optional testRepo = Optional.empty(); try { testRepo = Optional.ofNullable(gitService.getOrCheckoutRepository(exercise.getVcsTestRepositoryUri(), true)); diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisStatusUpdateService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisStatusUpdateService.java index ad1a63e60c58..258be6263b7e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisStatusUpdateService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisStatusUpdateService.java @@ -10,10 +10,12 @@ import org.springframework.stereotype.Service; import de.tum.cit.aet.artemis.iris.service.IrisCompetencyGenerationService; +import de.tum.cit.aet.artemis.iris.service.IrisRewritingService; import de.tum.cit.aet.artemis.iris.service.pyris.dto.chat.PyrisChatStatusUpdateDTO; import de.tum.cit.aet.artemis.iris.service.pyris.dto.chat.textexercise.PyrisTextExerciseChatStatusUpdateDTO; import de.tum.cit.aet.artemis.iris.service.pyris.dto.competency.PyrisCompetencyStatusUpdateDTO; import de.tum.cit.aet.artemis.iris.service.pyris.dto.lectureingestionwebhook.PyrisLectureIngestionStatusUpdateDTO; +import de.tum.cit.aet.artemis.iris.service.pyris.dto.rewriting.PyrisRewritingStatusUpdateDTO; import de.tum.cit.aet.artemis.iris.service.pyris.dto.status.PyrisStageDTO; import de.tum.cit.aet.artemis.iris.service.pyris.dto.status.PyrisStageState; import de.tum.cit.aet.artemis.iris.service.pyris.job.CompetencyExtractionJob; @@ -21,6 +23,7 @@ import de.tum.cit.aet.artemis.iris.service.pyris.job.ExerciseChatJob; import de.tum.cit.aet.artemis.iris.service.pyris.job.IngestionWebhookJob; import de.tum.cit.aet.artemis.iris.service.pyris.job.PyrisJob; +import de.tum.cit.aet.artemis.iris.service.pyris.job.RewritingJob; import de.tum.cit.aet.artemis.iris.service.pyris.job.TextExerciseChatJob; import de.tum.cit.aet.artemis.iris.service.pyris.job.TrackedSessionBasedPyrisJob; import de.tum.cit.aet.artemis.iris.service.session.IrisCourseChatSessionService; @@ -41,16 +44,19 @@ public class PyrisStatusUpdateService { private final IrisCompetencyGenerationService competencyGenerationService; + private final IrisRewritingService rewritingService; + private static final Logger log = LoggerFactory.getLogger(PyrisStatusUpdateService.class); public PyrisStatusUpdateService(PyrisJobService pyrisJobService, IrisExerciseChatSessionService irisExerciseChatSessionService, IrisTextExerciseChatSessionService irisTextExerciseChatSessionService, IrisCourseChatSessionService courseChatSessionService, - IrisCompetencyGenerationService competencyGenerationService) { + IrisCompetencyGenerationService competencyGenerationService, IrisRewritingService rewritingService) { this.pyrisJobService = pyrisJobService; this.irisExerciseChatSessionService = irisExerciseChatSessionService; this.irisTextExerciseChatSessionService = irisTextExerciseChatSessionService; this.courseChatSessionService = courseChatSessionService; this.competencyGenerationService = competencyGenerationService; + this.rewritingService = rewritingService; } /** @@ -105,6 +111,18 @@ public void handleStatusUpdate(CompetencyExtractionJob job, PyrisCompetencyStatu removeJobIfTerminatedElseUpdate(statusUpdate.stages(), updatedJob); } + /** + * Handles the status update of a rewriting job and forwards it to + * {@link IrisRewritingService#handleStatusUpdate(RewritingJob, PyrisRewritingStatusUpdateDTO)} + * + * @param job the job that is updated + * @param statusUpdate the status update + */ + public void handleStatusUpdate(RewritingJob job, PyrisRewritingStatusUpdateDTO statusUpdate) { + var updatedJob = rewritingService.handleStatusUpdate(job, statusUpdate); + removeJobIfTerminatedElseUpdate(statusUpdate.stages(), updatedJob); + } + /** * Removes the job from the job service if the status update indicates that the job is terminated; updates it to distribute changes otherwise. * A job is terminated if all stages are in a terminal state. diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/competency/PyrisCompetencyExtractionPipelineExecutionDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/competency/PyrisCompetencyExtractionPipelineExecutionDTO.java index 22fcaa3c1f9a..f0b401145bfd 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/competency/PyrisCompetencyExtractionPipelineExecutionDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/competency/PyrisCompetencyExtractionPipelineExecutionDTO.java @@ -6,7 +6,7 @@ import de.tum.cit.aet.artemis.iris.service.pyris.dto.PyrisPipelineExecutionDTO; /** - * DTO to execute the Iris competency extraction pipeline on Pyris + * DTO to execute the Iris rewriting pipeline on Pyris * * @param execution The pipeline execution details * @param courseDescription The description of the course diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/data/PyrisRewriteTextRequestDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/data/PyrisRewriteTextRequestDTO.java new file mode 100644 index 000000000000..1e7cd96bcaa5 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/data/PyrisRewriteTextRequestDTO.java @@ -0,0 +1,9 @@ +package de.tum.cit.aet.artemis.iris.service.pyris.dto.data; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.cit.aet.artemis.iris.service.pyris.dto.rewriting.RewritingVariant; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record PyrisRewriteTextRequestDTO(String toBeRewritten, RewritingVariant variant) { +} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/rewriting/PyrisRewritingPipelineExecutionDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/rewriting/PyrisRewritingPipelineExecutionDTO.java new file mode 100644 index 000000000000..6aae602ab5fe --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/rewriting/PyrisRewritingPipelineExecutionDTO.java @@ -0,0 +1,15 @@ +package de.tum.cit.aet.artemis.iris.service.pyris.dto.rewriting; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.cit.aet.artemis.iris.service.pyris.dto.PyrisPipelineExecutionDTO; + +/** + * DTO to execute the Iris rewriting pipeline on Pyris + * + * @param execution The pipeline execution details + * @param toBeRewritten The text to be rewritten + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record PyrisRewritingPipelineExecutionDTO(PyrisPipelineExecutionDTO execution, String toBeRewritten) { +} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/rewriting/PyrisRewritingStatusUpdateDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/rewriting/PyrisRewritingStatusUpdateDTO.java new file mode 100644 index 000000000000..084b65417cd3 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/rewriting/PyrisRewritingStatusUpdateDTO.java @@ -0,0 +1,21 @@ +package de.tum.cit.aet.artemis.iris.service.pyris.dto.rewriting; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.cit.aet.artemis.core.domain.LLMRequest; +import de.tum.cit.aet.artemis.iris.service.pyris.dto.status.PyrisStageDTO; + +/** + * DTO for the Iris rewriting feature. + * Pyris sends callback updates back to Artemis during rewriting of the text. These updates contain the current status of the rewriting process, + * which are then forwarded to the user via Websockets. + * + * @param stages List of stages of the generation process + * @param result The result of the rewriting process so far + * @param tokens List of token usages send by Pyris for tracking the token usage and cost + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record PyrisRewritingStatusUpdateDTO(List stages, String result, List tokens) { +} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/rewriting/RewritingVariant.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/rewriting/RewritingVariant.java new file mode 100644 index 000000000000..aeaaf20db46a --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/rewriting/RewritingVariant.java @@ -0,0 +1,5 @@ +package de.tum.cit.aet.artemis.iris.service.pyris.dto.rewriting; + +public enum RewritingVariant { + FAQ, PROBLEM_STATEMENT +} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/job/RewritingJob.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/job/RewritingJob.java new file mode 100644 index 000000000000..37016865cb8c --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/job/RewritingJob.java @@ -0,0 +1,22 @@ +package de.tum.cit.aet.artemis.iris.service.pyris.job; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.cit.aet.artemis.core.domain.Course; + +/** + * A Pyris job to rewrite a text. + * + * @param jobId the job id + * @param courseId the course in which the rewriting is being done + * @param userId the user who started the job + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record RewritingJob(String jobId, long courseId, long userId) implements PyrisJob { + + @Override + public boolean canAccess(Course course) { + return course.getId().equals(courseId); + } + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisRewritingResource.java b/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisRewritingResource.java new file mode 100644 index 000000000000..b3a437d964c6 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisRewritingResource.java @@ -0,0 +1,66 @@ +package de.tum.cit.aet.artemis.iris.web; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_IRIS; + +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import de.tum.cit.aet.artemis.core.repository.CourseRepository; +import de.tum.cit.aet.artemis.core.repository.UserRepository; +import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInCourse.EnforceAtLeastTutorInCourse; +import de.tum.cit.aet.artemis.iris.service.IrisRewritingService; +import de.tum.cit.aet.artemis.iris.service.pyris.dto.data.PyrisRewriteTextRequestDTO; + +/** + * REST controller for managing Markdown Rewritings. + */ +@Profile(PROFILE_IRIS) +@RestController +@RequestMapping("api/") +public class IrisRewritingResource { + + private static final Logger log = LoggerFactory.getLogger(IrisRewritingResource.class); + + private static final String ENTITY_NAME = "rewriting"; + + private final UserRepository userRepository; + + private final CourseRepository courseRepository; + + private final Optional irisRewritingService; + + public IrisRewritingResource(UserRepository userRepository, CourseRepository courseRepository, Optional irisRewritingService) { + this.userRepository = userRepository; + this.courseRepository = courseRepository; + this.irisRewritingService = irisRewritingService; + + } + + /** + * POST /courses/{courseId}/rewrite-text : Rewrite a given text. + * + * @param request the request containing the text to be rewritten and the corresponding variant + * @param courseId the id of the course + * @return the ResponseEntity with status 200 (OK) + */ + @EnforceAtLeastTutorInCourse + @PostMapping("courses/{courseId}/rewrite-text") + public ResponseEntity rewriteText(@RequestBody PyrisRewriteTextRequestDTO request, @PathVariable Long courseId) { + var rewritingService = irisRewritingService.orElseThrow(); + var user = userRepository.getUserWithGroupsAndAuthorities(); + var course = courseRepository.findByIdElseThrow(courseId); + rewritingService.executeRewritingPipeline(user, course, request.variant(), request.toBeRewritten()); + log.debug("REST request to rewrite text: {}", request.toBeRewritten()); + return ResponseEntity.ok().build(); + } + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/web/open/PublicPyrisStatusUpdateResource.java b/src/main/java/de/tum/cit/aet/artemis/iris/web/open/PublicPyrisStatusUpdateResource.java index 73e4520c32a6..3dabd0970310 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/web/open/PublicPyrisStatusUpdateResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/web/open/PublicPyrisStatusUpdateResource.java @@ -23,11 +23,13 @@ import de.tum.cit.aet.artemis.iris.service.pyris.dto.chat.textexercise.PyrisTextExerciseChatStatusUpdateDTO; import de.tum.cit.aet.artemis.iris.service.pyris.dto.competency.PyrisCompetencyStatusUpdateDTO; import de.tum.cit.aet.artemis.iris.service.pyris.dto.lectureingestionwebhook.PyrisLectureIngestionStatusUpdateDTO; +import de.tum.cit.aet.artemis.iris.service.pyris.dto.rewriting.PyrisRewritingStatusUpdateDTO; import de.tum.cit.aet.artemis.iris.service.pyris.job.CompetencyExtractionJob; import de.tum.cit.aet.artemis.iris.service.pyris.job.CourseChatJob; import de.tum.cit.aet.artemis.iris.service.pyris.job.ExerciseChatJob; import de.tum.cit.aet.artemis.iris.service.pyris.job.IngestionWebhookJob; import de.tum.cit.aet.artemis.iris.service.pyris.job.PyrisJob; +import de.tum.cit.aet.artemis.iris.service.pyris.job.RewritingJob; import de.tum.cit.aet.artemis.iris.service.pyris.job.TextExerciseChatJob; /** @@ -149,6 +151,31 @@ public ResponseEntity respondInTextExerciseChat(@PathVariable String runId return ResponseEntity.ok().build(); } + /** + * POST public/pyris/pipelines/rewriting/runs/:runId/status : Send the rewritten text in a status update + *

+ * Uses custom token based authentication. + * + * @param runId the ID of the job + * @param statusUpdateDTO the status update + * @param request the HTTP request + * @throws ConflictException if the run ID in the URL does not match the run ID in the request body + * @throws AccessForbiddenException if the token is invalid + * @return a {@link ResponseEntity} with status {@code 200 (OK)} + */ + @PostMapping("pipelines/rewriting/runs/{runId}/status") + @EnforceNothing + public ResponseEntity setRewritingJobStatus(@PathVariable String runId, @RequestBody PyrisRewritingStatusUpdateDTO statusUpdateDTO, HttpServletRequest request) { + var job = pyrisJobService.getAndAuthenticateJobFromHeaderElseThrow(request, RewritingJob.class); + if (!Objects.equals(job.jobId(), runId)) { + throw new ConflictException("Run ID in URL does not match run ID in request body", "Job", "runIdMismatch"); + } + + pyrisStatusUpdateService.handleStatusUpdate(job, statusUpdateDTO); + + return ResponseEntity.ok().build(); + } + /** * {@code POST /api/public/pyris/webhooks/ingestion/runs/{runId}/status} : Set the status of an Ingestion job. * diff --git a/src/main/webapp/app/faq/faq-update.component.html b/src/main/webapp/app/faq/faq-update.component.html index 51211a6bb77e..207f1d0c1221 100644 --- a/src/main/webapp/app/faq/faq-update.component.html +++ b/src/main/webapp/app/faq/faq-update.component.html @@ -21,7 +21,8 @@

diff --git a/src/main/webapp/app/faq/faq-update.component.ts b/src/main/webapp/app/faq/faq-update.component.ts index 086feb0e9fdc..ea0d03c191e9 100644 --- a/src/main/webapp/app/faq/faq-update.component.ts +++ b/src/main/webapp/app/faq/faq-update.component.ts @@ -1,7 +1,7 @@ -import { Component, OnInit, inject } from '@angular/core'; +import { Component, OnInit, computed, inject } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; -import { Observable } from 'rxjs'; +import { Observable, map } from 'rxjs'; import { AlertService } from 'app/core/util/alert.service'; import { onError } from 'app/shared/util/global.utils'; import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; @@ -16,6 +16,12 @@ import { ArtemisCategorySelectorModule } from 'app/shared/category-selector/cate import { ArtemisSharedModule } from 'app/shared/shared.module'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { AccountService } from 'app/core/auth/account.service'; +import { RewriteAction } from 'app/shared/monaco-editor/model/actions/artemis-intelligence/rewrite.action'; +import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; +import { PROFILE_IRIS } from 'app/app.constants'; +import RewritingVariant from 'app/shared/monaco-editor/model/actions/artemis-intelligence/rewriting-variant'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { ArtemisIntelligenceService } from 'app/shared/monaco-editor/model/actions/artemis-intelligence/artemis-intelligence.service'; @Component({ selector: 'jhi-faq-update', @@ -24,6 +30,15 @@ import { AccountService } from 'app/core/auth/account.service'; imports: [ArtemisSharedModule, ArtemisSharedComponentModule, ArtemisMarkdownEditorModule, ArtemisCategorySelectorModule], }) export class FaqUpdateComponent implements OnInit { + private alertService = inject(AlertService); + private faqService = inject(FaqService); + private activatedRoute = inject(ActivatedRoute); + private navigationUtilService = inject(ArtemisNavigationUtilService); + private router = inject(Router); + private profileService = inject(ProfileService); + private accountService = inject(AccountService); + private artemisIntelligenceService = inject(ArtemisIntelligenceService); + faq: Faq; isSaving: boolean; isAllowedToSave: boolean; @@ -33,18 +48,14 @@ export class FaqUpdateComponent implements OnInit { isAtLeastInstructor = false; domainActionsDescription = [new FormulaAction()]; + irisEnabled = toSignal(this.profileService.getProfileInfo().pipe(map((profileInfo) => profileInfo.activeProfiles.includes(PROFILE_IRIS))), { initialValue: false }); + artemisIntelligenceActions = computed(() => (this.irisEnabled() ? [new RewriteAction(this.artemisIntelligenceService, RewritingVariant.FAQ, this.courseId)] : [])); + // Icons readonly faQuestionCircle = faQuestionCircle; readonly faSave = faSave; readonly faBan = faBan; - private alertService = inject(AlertService); - private faqService = inject(FaqService); - private activatedRoute = inject(ActivatedRoute); - private navigationUtilService = inject(ArtemisNavigationUtilService); - private router = inject(Router); - private accountService = inject(AccountService); - ngOnInit() { this.isSaving = false; this.courseId = Number(this.activatedRoute.snapshot.paramMap.get('courseId')); diff --git a/src/main/webapp/app/icons/icons.ts b/src/main/webapp/app/icons/icons.ts index cabc6c1d4613..700580c34027 100644 --- a/src/main/webapp/app/icons/icons.ts +++ b/src/main/webapp/app/icons/icons.ts @@ -56,9 +56,22 @@ export const facDetails: IconDefinition = { ], } as IconDefinition; +export const facArtemisIntelligence: IconDefinition = { + prefix: 'fac' as IconPrefix, + iconName: 'artemis-intelligence' as IconName, + icon: [ + 24, // SVG view box width + 24, // SVG view box height + [], + '', + 'M6.56973 16.902H7.44723C7.74423 16.902 7.96023 16.8705 8.09523 16.8075C8.23023 16.7445 8.32923 16.6005 8.39223 16.3755L9.85023 10.7865C9.88623 10.6425 9.92223 10.4985 9.95823 10.3545C9.99423 10.2105 10.0257 10.0665 10.0527 9.9225H10.1067C10.1337 10.0665 10.1652 10.2105 10.2012 10.3545C10.2372 10.4985 10.2732 10.6425 10.3092 10.7865L11.7267 16.3755C11.7897 16.6005 11.8977 16.7445 12.0507 16.8075C12.2127 16.8705 12.4242 16.902 12.6852 16.902H13.6167C13.9227 16.902 14.1252 16.866 14.2242 16.794C14.3322 16.722 14.3862 16.623 14.3862 16.497C14.3862 16.407 14.3727 16.3125 14.3457 16.2135C14.3187 16.1055 14.2827 15.993 14.2377 15.876L11.8077 8.046C11.7177 7.767 11.5962 7.5915 11.4432 7.5195C11.2992 7.4385 11.0787 7.398 10.7817 7.398H9.40473C9.11673 7.398 8.89623 7.4385 8.74323 7.5195C8.59023 7.5915 8.46873 7.767 8.37873 8.046L5.96223 15.876C5.92623 15.993 5.89473 16.1055 5.86773 16.2135C5.83173 16.3125 5.81373 16.407 5.81373 16.497C5.81373 16.623 5.86323 16.722 5.96223 16.794C6.07023 16.866 6.27273 16.902 6.56973 16.902ZM7.77123 15.2415H12.3882V13.1895H7.77123V15.2415Z M15.9695 16.902C15.5915 16.902 15.317 16.8525 15.146 16.7535C14.975 16.6455 14.8895 16.5015 14.8895 16.3215V7.9785C14.8895 7.7985 14.975 7.659 15.146 7.56C15.317 7.452 15.5915 7.398 15.9695 7.398H16.307C16.685 7.398 16.9595 7.452 17.1305 7.56C17.3015 7.659 17.387 7.7985 17.387 7.9785V16.3215C17.387 16.5015 17.3015 16.6455 17.1305 16.7535C16.9595 16.8525 16.685 16.902 16.307 16.902H15.9695Z M7.125 0C7.74844 0 8.25 0.501562 8.25 1.125V3H10.875V1.125C10.875 0.501562 11.3766 0 12 0C12.6234 0 13.125 0.501562 13.125 1.125V3H15.75V1.125C15.75 0.501562 16.2516 0 16.875 0C17.4984 0 18 0.501562 18 1.125V3C19.6547 3 21 4.34531 21 6H22.875C23.4984 6 24 6.50156 24 7.125C24 7.74844 23.4984 8.25 22.875 8.25H21V10.875H22.875C23.4984 10.875 24 11.3766 24 12C24 12.6234 23.4984 13.125 22.875 13.125H21V15.75H22.875C23.4984 15.75 24 16.2516 24 16.875C24 17.4984 23.4984 18 22.875 18H21C21 19.6547 19.6547 21 18 21V22.875C18 23.4984 17.4984 24 16.875 24C16.2516 24 15.75 23.4984 15.75 22.875V21H13.125V22.875C13.125 23.4984 12.6234 24 12 24C11.3766 24 10.875 23.4984 10.875 22.875V21H8.25V22.875C8.25 23.4984 7.74844 24 7.125 24C6.50156 24 6 23.4984 6 22.875V21C4.34531 21 3 19.6547 3 18H1.125C0.501562 18 0 17.4984 0 16.875C0 16.2516 0.501562 15.75 1.125 15.75H3V13.125H1.125C0.501562 13.125 0 12.6234 0 12C0 11.3766 0.501562 10.875 1.125 10.875H3V8.25H1.125C0.501562 8.25 0 7.74844 0 7.125C0 6.50156 0.501562 6 1.125 6H3C3 4.34531 4.34531 3 6 3V1.125C6 0.501562 6.50156 0 7.125 0ZM6.32812 5.10938C5.65503 5.10938 5.10938 5.65503 5.10938 6.32812V17.7188C5.10938 18.3918 5.65503 18.9375 6.32812 18.9375H17.7188C18.3918 18.9375 18.9375 18.3918 18.9375 17.7188V6.32812C18.9375 5.65503 18.3918 5.10938 17.7188 5.10938H6.32812Z', + ], +} as IconDefinition; + export const artemisIconPack: IconPack = { facSidebar, facSaveSuccess, facSaveWarning, facDetails, + facArtemisIntelligence, }; diff --git a/src/main/webapp/app/shared/markdown-editor/monaco/markdown-editor-monaco.component.html b/src/main/webapp/app/shared/markdown-editor/monaco/markdown-editor-monaco.component.html index 9cfeb910578a..a7b15ad2903b 100644 --- a/src/main/webapp/app/shared/markdown-editor/monaco/markdown-editor-monaco.component.html +++ b/src/main/webapp/app/shared/markdown-editor/monaco/markdown-editor-monaco.component.html @@ -201,6 +201,30 @@ } } + + @if (displayedActions.artemisIntelligence.length > 0) { +
+ @if (artemisIntelligenceService.isLoading()) { + + } @else { + + + } +
+ } @for (action of displayedActions.meta; track action.id) { diff --git a/src/main/webapp/app/shared/markdown-editor/monaco/markdown-editor-monaco.component.ts b/src/main/webapp/app/shared/markdown-editor/monaco/markdown-editor-monaco.component.ts index bae7891424ab..31922986d8fc 100644 --- a/src/main/webapp/app/shared/markdown-editor/monaco/markdown-editor-monaco.component.ts +++ b/src/main/webapp/app/shared/markdown-editor/monaco/markdown-editor-monaco.component.ts @@ -41,7 +41,7 @@ import { AttachmentAction } from 'app/shared/monaco-editor/model/actions/attachm import { BulletedListAction } from 'app/shared/monaco-editor/model/actions/bulleted-list.action'; import { StrikethroughAction } from 'app/shared/monaco-editor/model/actions/strikethrough.action'; import { OrderedListAction } from 'app/shared/monaco-editor/model/actions/ordered-list.action'; -import { faAngleDown, faGripLines, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; +import { faAngleDown, faGripLines, faQuestionCircle, faSpinner } from '@fortawesome/free-solid-svg-icons'; import { v4 as uuid } from 'uuid'; import { FileUploadResponse, FileUploaderService } from 'app/shared/http/file-uploader.service'; import { AlertService, AlertType } from 'app/core/util/alert.service'; @@ -70,6 +70,8 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu'; import { MatButton } from '@angular/material/button'; import { ArtemisTranslatePipe } from '../../pipes/artemis-translate.pipe'; +import { ArtemisIntelligenceService } from 'app/shared/monaco-editor/model/actions/artemis-intelligence/artemis-intelligence.service'; +import { facArtemisIntelligence } from 'app/icons/icons'; export enum MarkdownEditorHeight { INLINE = 125, @@ -89,6 +91,8 @@ interface MarkdownActionsByGroup { }; // Special case due to the complex structure of lectures, attachments, and their slides lecture?: LectureAttachmentReferenceAction; + // AI assistance in the editor + artemisIntelligence: TextEditorAction[]; meta: TextEditorAction[]; } @@ -140,6 +144,7 @@ export class MarkdownEditorMonacoComponent implements AfterContentInit, AfterVie private readonly metisService = inject(MetisService, { optional: true }); private readonly fileUploaderService = inject(FileUploaderService); private readonly artemisMarkdown = inject(ArtemisMarkdownService); + protected readonly artemisIntelligenceService = inject(ArtemisIntelligenceService); @ViewChild(MonacoEditorComponent, { static: false }) monacoEditor: MonacoEditorComponent; @ViewChild('fullElement', { static: true }) fullElement: ElementRef; @@ -228,6 +233,9 @@ export class MarkdownEditorMonacoComponent implements AfterContentInit, AfterVie @Input() domainActions: TextEditorDomainAction[] = []; + @Input() + artemisIntelligenceActions: TextEditorAction[] = []; + @Input() metaActions: TextEditorAction[] = [new FullscreenAction()]; @@ -271,6 +279,7 @@ export class MarkdownEditorMonacoComponent implements AfterContentInit, AfterVie color: undefined, domain: { withoutOptions: [], withOptions: [] }, lecture: undefined, + artemisIntelligence: [], meta: [], }; @@ -295,8 +304,10 @@ export class MarkdownEditorMonacoComponent implements AfterContentInit, AfterVie readonly colorPickerHeight = 110; // Icons protected readonly faQuestionCircle = faQuestionCircle; + protected readonly faSpinner = faSpinner; protected readonly faGripLines = faGripLines; protected readonly faAngleDown = faAngleDown; + protected readonly facArtemisIntelligence = facArtemisIntelligence; // Types and values exposed to the template protected readonly LectureUnitType = LectureUnitType; protected readonly ReferenceType = ReferenceType; @@ -325,6 +336,7 @@ export class MarkdownEditorMonacoComponent implements AfterContentInit, AfterVie ) as TextEditorDomainActionWithOptions[], }, lecture: this.filterDisplayedAction(this.lectureReferenceAction), + artemisIntelligence: this.filterDisplayedActions(this.artemisIntelligenceActions ?? []), meta: this.filterDisplayedActions(this.metaActions), }; } @@ -368,6 +380,7 @@ export class MarkdownEditorMonacoComponent implements AfterContentInit, AfterVie this.domainActions, ...(this.colorAction ? [this.colorAction] : []), ...(this.lectureReferenceAction ? [this.lectureReferenceAction] : []), + ...this.artemisIntelligenceActions, this.metaActions, ] .flat() diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/adapter/monaco-text-editor.adapter.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/adapter/monaco-text-editor.adapter.ts index 0f1456bec524..ea28aa20229e 100644 --- a/src/main/webapp/app/shared/monaco-editor/model/actions/adapter/monaco-text-editor.adapter.ts +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/adapter/monaco-text-editor.adapter.ts @@ -232,4 +232,11 @@ export class MonacoTextEditorAdapter implements TextEditor { const modifier = MonacoTextEditorAdapter.MODIFIER_MAP.get(keybinding.getModifier()) ?? 0; return keyCode | modifier; } + + getFullText(): string { + const firstPosition = new TextEditorPosition(1, 1); + const lastPosition = this.getEndPosition(); + const range = new TextEditorRange(firstPosition, lastPosition); + return this.getTextAtRange(range); + } } diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/adapter/text-editor.interface.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/adapter/text-editor.interface.ts index ff6c204b4414..c06d9a77aabe 100644 --- a/src/main/webapp/app/shared/monaco-editor/model/actions/adapter/text-editor.interface.ts +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/adapter/text-editor.interface.ts @@ -53,6 +53,12 @@ export interface TextEditor { */ getTextAtRange(range: TextEditorRange): string; + /** + * Retrieves the full text of the editor. + * Line endings are normalized to '\n'. + */ + getFullText(): string; + /** * Retrieves the text of the line at the given line number in the editor. * @param lineNumber The line number to get the text from. Line numbers start at 1. diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/artemis-intelligence/artemis-intelligence.service.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/artemis-intelligence/artemis-intelligence.service.ts new file mode 100644 index 000000000000..3aca4d2edbca --- /dev/null +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/artemis-intelligence/artemis-intelligence.service.ts @@ -0,0 +1,66 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable, computed, inject, signal } from '@angular/core'; +import { Observable } from 'rxjs'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; +import RewritingVariant from 'app/shared/monaco-editor/model/actions/artemis-intelligence/rewriting-variant'; +import { AlertService } from 'app/core/util/alert.service'; + +/** + * Service providing shared functionality for Artemis Intelligence of the markdown editor. + * This service is intended to be used by the AI actions of the Monaco editors. + */ +@Injectable({ providedIn: 'root' }) +export class ArtemisIntelligenceService { + public resourceUrl = 'api/courses'; + + private http = inject(HttpClient); + private jhiWebsocketService = inject(WebsocketService); + private alertService = inject(AlertService); + + private isLoadingRewrite = signal(false); + isLoading = computed(() => this.isLoadingRewrite()); + + /** + * Triggers the rewriting pipeline via HTTP and subscribes to its WebSocket updates. + * @param toBeRewritten The text to be rewritten. + * @param rewritingVariant The variant for rewriting. + * @param courseId The ID of the course to which the rewritten text belongs. + * @return Observable that emits the rewritten text when available. + */ + rewrite(toBeRewritten: string, rewritingVariant: RewritingVariant, courseId: number): Observable { + this.isLoadingRewrite.set(true); + return new Observable((observer) => { + this.http + .post(`${this.resourceUrl}/${courseId}/rewrite-text`, { + toBeRewritten: toBeRewritten, + variant: rewritingVariant, + }) + .subscribe({ + next: () => { + const websocketTopic = `/user/topic/iris/rewriting/${courseId}`; + this.jhiWebsocketService.subscribe(websocketTopic); + this.jhiWebsocketService.receive(websocketTopic).subscribe({ + next: (update: any) => { + if (update.result) { + observer.next(update.result); + observer.complete(); + this.isLoadingRewrite.set(false); + this.jhiWebsocketService.unsubscribe(websocketTopic); + this.alertService.success('artemisApp.markdownEditor.artemisIntelligence.alerts.rewrite.success'); + } + }, + error: (error) => { + observer.error(error); + this.isLoadingRewrite.set(false); + this.jhiWebsocketService.unsubscribe(websocketTopic); + }, + }); + }, + error: (error) => { + this.isLoadingRewrite.set(false); + observer.error(error); + }, + }); + }); + } +} diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/artemis-intelligence/rewrite.action.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/artemis-intelligence/rewrite.action.ts new file mode 100644 index 000000000000..73ac1525d675 --- /dev/null +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/artemis-intelligence/rewrite.action.ts @@ -0,0 +1,29 @@ +import { TextEditorAction } from 'app/shared/monaco-editor/model/actions/text-editor-action.model'; +import { TextEditor } from 'app/shared/monaco-editor/model/actions/adapter/text-editor.interface'; +import RewritingVariant from 'app/shared/monaco-editor/model/actions/artemis-intelligence/rewriting-variant'; +import { ArtemisIntelligenceService } from 'app/shared/monaco-editor/model/actions/artemis-intelligence/artemis-intelligence.service'; + +/** + * Artemis Intelligence action for rewriting in the editor. + */ +export class RewriteAction extends TextEditorAction { + static readonly ID = 'artemisIntelligence.rewrite.action'; + + element?: HTMLElement; + + constructor( + private readonly artemisIntelligenceService: ArtemisIntelligenceService, + private readonly rewritingVariant: RewritingVariant, + private readonly courseId: number, + ) { + super(RewriteAction.ID, 'artemisApp.markdownEditor.artemisIntelligence.commands.rewrite'); + } + + /** + * Runs the rewriting of the markdown content of the editor. + * @param editor The editor in which to rewrite the markdown. + */ + run(editor: TextEditor): void { + this.rewriteMarkdown(editor, this.artemisIntelligenceService, this.rewritingVariant, this.courseId); + } +} diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/artemis-intelligence/rewriting-variant.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/artemis-intelligence/rewriting-variant.ts new file mode 100644 index 000000000000..80ab65fddeed --- /dev/null +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/artemis-intelligence/rewriting-variant.ts @@ -0,0 +1,6 @@ +enum RewritingVariant { + FAQ = 'FAQ', + PROBLEM_STATEMENT = 'PROBLEM_STATEMENT', +} + +export default RewritingVariant; diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/text-editor-action.model.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/text-editor-action.model.ts index 62e037868ce0..fe5da5e71942 100644 --- a/src/main/webapp/app/shared/monaco-editor/model/actions/text-editor-action.model.ts +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/text-editor-action.model.ts @@ -7,6 +7,8 @@ import { TextEditorRange } from 'app/shared/monaco-editor/model/actions/adapter/ import { TextEditorPosition } from 'app/shared/monaco-editor/model/actions/adapter/text-editor-position.model'; import { TextEditorCompletionItem } from 'app/shared/monaco-editor/model/actions/adapter/text-editor-completion-item.model'; import { TextEditorKeybinding } from 'app/shared/monaco-editor/model/actions/adapter/text-editor-keybinding.model'; +import RewritingVariant from 'app/shared/monaco-editor/model/actions/artemis-intelligence/rewriting-variant'; +import { ArtemisIntelligenceService } from 'app/shared/monaco-editor/model/actions/artemis-intelligence/artemis-intelligence.service'; export abstract class TextEditorAction implements Disposable { id: string; @@ -293,4 +295,22 @@ export abstract class TextEditorAction implements Disposable { } editor.layout(); } + + /** + * Rewrites the given text using Artemis Intelligence. If no text is provided, the editor's will return undefined. + * @param editor The editor to toggle the text rewriting for. + * @param artemisIntelligence The Artemis Intelligence service to use for rewriting the text. + * @param variant The variant to use for rewriting the text. + * @param courseId The ID of the course to use for rewriting the text (for tracking purposes). + */ + rewriteMarkdown(editor: TextEditor, artemisIntelligence: ArtemisIntelligenceService, variant: RewritingVariant, courseId: number): void { + const text = editor.getFullText(); + if (text) { + artemisIntelligence.rewrite(text, variant, courseId).subscribe({ + next: (message) => { + this.replaceTextAtRange(editor, new TextEditorRange(new TextEditorPosition(1, 1), this.getEndPosition(editor)), message); + }, + }); + } + } } diff --git a/src/main/webapp/i18n/de/markdownEditor.json b/src/main/webapp/i18n/de/markdownEditor.json index d2d331be87b7..aa77392955f3 100644 --- a/src/main/webapp/i18n/de/markdownEditor.json +++ b/src/main/webapp/i18n/de/markdownEditor.json @@ -12,7 +12,18 @@ "slideNotFound": "Referenzierte Folie wurde nicht gefunden", "imageNotFound": "Referenziertes Bild wurde nicht gefunden" }, - "slideWithNumber": "Folie {{ number }}" + "slideWithNumber": "Folie {{ number }}", + "artemisIntelligence": { + "tooltip": "Artemis Intelligence", + "commands": { + "rewrite": "Umformulieren" + }, + "alerts": { + "rewrite": { + "success": "Der Text wurde umgeschrieben. Drücke Strg+Z / Cmd+Z, um den Originaltext wiederherzustellen." + } + } + } } } } diff --git a/src/main/webapp/i18n/en/markdownEditor.json b/src/main/webapp/i18n/en/markdownEditor.json index 26c9c39e7fc3..44666915a0d7 100644 --- a/src/main/webapp/i18n/en/markdownEditor.json +++ b/src/main/webapp/i18n/en/markdownEditor.json @@ -12,7 +12,18 @@ "slideNotFound": "Referenced slide was not found", "imageNotFound": "Referenced image was not found" }, - "slideWithNumber": "Slide {{ number }}" + "slideWithNumber": "Slide {{ number }}", + "artemisIntelligence": { + "tooltip": "Artemis Intelligence", + "commands": { + "rewrite": "Rewrite" + }, + "alerts": { + "rewrite": { + "success": "The text was rewritten. Press Ctrl+Z / Cmd+Z to restore the original text." + } + } + } } } } diff --git a/src/test/java/de/tum/cit/aet/artemis/core/connector/IrisRequestMockProvider.java b/src/test/java/de/tum/cit/aet/artemis/core/connector/IrisRequestMockProvider.java index db8e032aca21..a71d07785f7b 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/connector/IrisRequestMockProvider.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/connector/IrisRequestMockProvider.java @@ -37,6 +37,7 @@ import de.tum.cit.aet.artemis.iris.service.pyris.dto.chat.textexercise.PyrisTextExerciseChatPipelineExecutionDTO; import de.tum.cit.aet.artemis.iris.service.pyris.dto.competency.PyrisCompetencyExtractionPipelineExecutionDTO; import de.tum.cit.aet.artemis.iris.service.pyris.dto.lectureingestionwebhook.PyrisWebhookLectureIngestionExecutionDTO; +import de.tum.cit.aet.artemis.iris.service.pyris.dto.rewriting.PyrisRewritingPipelineExecutionDTO; @Component @Profile(PROFILE_IRIS) @@ -162,6 +163,20 @@ public void mockRunCompetencyExtractionResponseAnd(Consumer executionDTOConsumer) { + // @formatter:off + mockServer + .expect(ExpectedCount.once(), requestTo(pipelinesApiURL + "/rewriting/faq/run")) + .andExpect(method(HttpMethod.POST)) + .andRespond(request -> { + var mockRequest = (MockClientHttpRequest) request; + var dto = mapper.readValue(mockRequest.getBodyAsString(), PyrisRewritingPipelineExecutionDTO.class); + executionDTOConsumer.accept(dto); + return MockRestResponseCreators.withRawStatus(HttpStatus.ACCEPTED.value()).createResponse(request); + }); + // @formatter:on + } + public void mockIngestionWebhookRunResponse(Consumer responseConsumer) { mockServer.expect(ExpectedCount.once(), requestTo(webhooksApiURL + "/lectures/fullIngestion")).andExpect(method(HttpMethod.POST)).andRespond(request -> { var mockRequest = (MockClientHttpRequest) request; diff --git a/src/test/java/de/tum/cit/aet/artemis/iris/architecture/IrisCodeStyleArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/iris/architecture/IrisCodeStyleArchitectureTest.java index 28c42c67d205..9ed370ccf0c3 100644 --- a/src/test/java/de/tum/cit/aet/artemis/iris/architecture/IrisCodeStyleArchitectureTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/iris/architecture/IrisCodeStyleArchitectureTest.java @@ -16,6 +16,6 @@ protected int dtoAsAnnotatedRecordThreshold() { @Override protected int dtoNameEndingThreshold() { - return 4; + return 5; } } diff --git a/src/test/javascript/spec/component/faq/faq-update.component.spec.ts b/src/test/javascript/spec/component/faq/faq-update.component.spec.ts index 275bd4cb4f48..df52bcd9683d 100644 --- a/src/test/javascript/spec/component/faq/faq-update.component.spec.ts +++ b/src/test/javascript/spec/component/faq/faq-update.component.spec.ts @@ -16,6 +16,8 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { AlertService } from 'app/core/util/alert.service'; import { FaqCategory } from 'app/entities/faq-category.model'; import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module'; +import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; +import { ProfileInfo } from 'app/shared/layouts/profiles/profile-info.model'; describe('FaqUpdateComponent', () => { let faqUpdateComponentFixture: ComponentFixture; @@ -36,6 +38,7 @@ describe('FaqUpdateComponent', () => { faq1.questionAnswer = 'questionAnswer'; faq1.categories = [new FaqCategory('category1', '#94a11c')]; courseId = 1; + const mockProfileInfo = { activeProfiles: ['iris'] } as ProfileInfo; TestBed.configureTestingModule({ imports: [ArtemisTestModule, MockModule(ArtemisMarkdownEditorModule), MockModule(BrowserAnimationsModule)], declarations: [FaqUpdateComponent, MockPipe(HtmlForMarkdownPipe), MockRouterLinkDirective], @@ -73,6 +76,10 @@ describe('FaqUpdateComponent', () => { ); }, }), + + MockProvider(ProfileService, { + getProfileInfo: () => of(mockProfileInfo), + }), ], }).compileComponents(); @@ -242,4 +249,10 @@ describe('FaqUpdateComponent', () => { expect(alertServiceStub).toHaveBeenCalledOnce(); flush(); })); + + it('should handleMarkdownChange properly ', () => { + faqUpdateComponent.faq = { questionTitle: 'test1', questionAnswer: 'answer' } as Faq; + faqUpdateComponent.handleMarkdownChange('test'); + expect(faqUpdateComponent.faq.questionAnswer).toEqual('test'); + }); }); diff --git a/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts b/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts index 276d3e82fd7d..216be110cf91 100644 --- a/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts @@ -84,6 +84,7 @@ describe('PostingsMarkdownEditor', () => { revealRange: jest.fn(), addCompleter: jest.fn(), addPasteListener: jest.fn(), + getFullText: jest.fn(), }; const mockPositionStrategy = { diff --git a/src/test/javascript/spec/service/artemis-intelligence.service.spec.ts b/src/test/javascript/spec/service/artemis-intelligence.service.spec.ts new file mode 100644 index 000000000000..8c3635ca6e3f --- /dev/null +++ b/src/test/javascript/spec/service/artemis-intelligence.service.spec.ts @@ -0,0 +1,75 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { provideHttpClient } from '@angular/common/http'; +import { ArtemisTestModule } from '../test.module'; + +import { ArtemisIntelligenceService } from 'app/shared/monaco-editor/model/actions/artemis-intelligence/artemis-intelligence.service'; +import { BehaviorSubject } from 'rxjs'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; +import RewritingVariant from 'app/shared/monaco-editor/model/actions/artemis-intelligence/rewriting-variant'; + +describe('ArtemisIntelligenceService', () => { + let httpMock: HttpTestingController; + let service: ArtemisIntelligenceService; + + beforeEach(() => { + const mockWebsocketService = { + subscribe: jest.fn(), + unsubscribe: jest.fn(), + receive: jest.fn().mockReturnValue(new BehaviorSubject({ result: 'Rewritten Text' })), + }; + + TestBed.configureTestingModule({ + imports: [ArtemisTestModule], + providers: [provideHttpClient(), provideHttpClientTesting(), { provide: WebsocketService, useValue: mockWebsocketService }], + }); + + httpMock = TestBed.inject(HttpTestingController); + service = TestBed.inject(ArtemisIntelligenceService); + }); + + afterEach(() => { + httpMock.verify(); + }); + + describe('Service methods', () => { + it('should trigger rewriting pipeline and return rewritten text', () => { + const toBeRewritten = 'OriginalText'; + const rewritingVariant = RewritingVariant.FAQ; + const courseId = 1; + + service.rewrite(toBeRewritten, rewritingVariant, courseId).subscribe((rewrittenText) => { + expect(rewrittenText).toBe('Rewritten Text'); + }); + + const req = httpMock.expectOne({ + url: `api/courses/${courseId}/rewrite-text`, + method: 'POST', + }); + + req.flush(null); + }); + + it('should handle HTTP error correctly', () => { + const toBeRewritten = 'OriginalText'; + const rewritingVariant = RewritingVariant.FAQ; + const courseId = 1; + + service.rewrite(toBeRewritten, rewritingVariant, courseId).subscribe({ + next: () => { + throw new Error('Expected error, but got success response'); + }, + error: (err) => { + expect(err).toBe('HTTP Request Error:'); + }, + }); + + const req = httpMock.expectOne({ + url: `api/courses/${courseId}/rewrite-text`, + method: 'POST', + }); + + req.flush(null, { status: 400, statusText: 'Bad Request' }); + }); + }); +}); From f2bff3b8ebf7735f6b894079f86f1fb23eddc171 Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Thu, 30 Jan 2025 16:32:31 +0100 Subject: [PATCH 2/8] Programming exercises: Add Artemis intelligence rewriting for problem statement (#10156) --- ...ercise-editable-instruction.component.html | 1 + ...exercise-editable-instruction.component.ts | 45 ++++++++++++++++--- ...ise-editable-instruction.component.spec.ts | 20 ++++++++- .../mock-activated-route-with-subjects.ts | 4 +- ...code-editor-instructor.integration.spec.ts | 9 +++- 5 files changed, 70 insertions(+), 9 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/manage/instructions-editor/programming-exercise-editable-instruction.component.html b/src/main/webapp/app/exercises/programming/manage/instructions-editor/programming-exercise-editable-instruction.component.html index 677106c40346..e2c6ba9e9f43 100644 --- a/src/main/webapp/app/exercises/programming/manage/instructions-editor/programming-exercise-editable-instruction.component.html +++ b/src/main/webapp/app/exercises/programming/manage/instructions-editor/programming-exercise-editable-instruction.component.html @@ -4,6 +4,7 @@ profileInfo.activeProfiles.includes(PROFILE_IRIS))), { initialValue: false }); + artemisIntelligenceActions = computed(() => + this.irisEnabled() ? [new RewriteAction(this.artemisIntelligenceService, RewritingVariant.PROBLEM_STATEMENT, this.courseId)] : [], + ); + savingInstructions = false; unsavedChangesValue = false; @@ -117,6 +148,10 @@ export class ProgrammingExerciseEditableInstructionComponent implements AfterVie protected readonly MarkdownEditorHeight = MarkdownEditorHeight; + ngOnInit() { + this.courseId = Number(this.activatedRoute.snapshot.paramMap.get('courseId')); + } + ngOnChanges(changes: SimpleChanges): void { if (hasExerciseChanged(changes)) { this.setupTestCaseSubscription(); @@ -236,9 +271,9 @@ export class ProgrammingExerciseEditableInstructionComponent implements AfterVie loadTestCasesFromTemplateParticipationResult = (templateParticipationId: number): Observable> => { // Fallback for exercises that don't have test cases yet. return this.programmingExerciseParticipationService.getLatestResultWithFeedback(templateParticipationId).pipe( - rxMap((result) => (!result?.feedbacks ? throwError(() => new Error('no result available')) : result)), + map((result) => (!result?.feedbacks ? throwError(() => new Error('no result available')) : result)), // use the text (legacy case) or the name of the provided test case attribute - rxMap(({ feedbacks }: Result) => feedbacks!.map((feedback) => feedback.text ?? feedback.testCase?.testName).sort()), + map(({ feedbacks }: Result) => feedbacks!.map((feedback) => feedback.text ?? feedback.testCase?.testName).sort()), catchError(() => of([])), ); }; diff --git a/src/test/javascript/spec/component/programming-exercise/programming-exercise-editable-instruction.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/programming-exercise-editable-instruction.component.spec.ts index c00bb591678c..f45afcd4c876 100644 --- a/src/test/javascript/spec/component/programming-exercise/programming-exercise-editable-instruction.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/programming-exercise-editable-instruction.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed, fakeAsync, flush, tick } from '@angular/core import { TranslateService } from '@ngx-translate/core'; import { By } from '@angular/platform-browser'; import { ProgrammingExerciseService } from 'app/exercises/programming/manage/services/programming-exercise.service'; -import { MockComponent, MockDirective, MockPipe } from 'ng-mocks'; +import { MockComponent, MockDirective, MockPipe, MockProvider } from 'ng-mocks'; import { Subject, of, throwError } from 'rxjs'; import { DebugElement } from '@angular/core'; import { ArtemisTestModule } from '../../test.module'; @@ -28,6 +28,9 @@ import { HttpResponse } from '@angular/common/http'; import { AlertService } from 'app/core/util/alert.service'; import { MockAlertService } from '../../helpers/mocks/service/mock-alert.service'; import { MarkdownEditorMonacoComponent } from 'app/shared/markdown-editor/monaco/markdown-editor-monaco.component'; +import { ActivatedRoute, convertToParamMap } from '@angular/router'; +import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; +import { ProfileInfo } from '../../../../../main/webapp/app/shared/layouts/profiles/profile-info.model'; describe('ProgrammingExerciseEditableInstructionComponent', () => { let comp: ProgrammingExerciseEditableInstructionComponent; @@ -53,6 +56,17 @@ describe('ProgrammingExerciseEditableInstructionComponent', () => { { testName: 'test3', active: false }, ]; + const mockProfileInfo = { activeProfiles: ['iris'] } as ProfileInfo; + + const route = { + snapshot: { paramMap: convertToParamMap({ courseId: '1' }) }, + url: { + pipe: () => ({ + subscribe: () => {}, + }), + }, + } as ActivatedRoute; + beforeEach(() => { return TestBed.configureTestingModule({ imports: [ArtemisTestModule, MockDirective(NgbTooltip)], @@ -69,6 +83,10 @@ describe('ProgrammingExerciseEditableInstructionComponent', () => { { provide: ParticipationWebsocketService, useClass: MockParticipationWebsocketService }, { provide: TranslateService, useClass: MockTranslateService }, { provide: AlertService, useClass: MockAlertService }, + { provide: ActivatedRoute, useValue: route }, + MockProvider(ProfileService, { + getProfileInfo: () => of(mockProfileInfo), + }), ], }) .compileComponents() diff --git a/src/test/javascript/spec/helpers/mocks/activated-route/mock-activated-route-with-subjects.ts b/src/test/javascript/spec/helpers/mocks/activated-route/mock-activated-route-with-subjects.ts index 28ca21214cc9..2a766c7b24c2 100644 --- a/src/test/javascript/spec/helpers/mocks/activated-route/mock-activated-route-with-subjects.ts +++ b/src/test/javascript/spec/helpers/mocks/activated-route/mock-activated-route-with-subjects.ts @@ -1,10 +1,10 @@ import { Subject } from 'rxjs'; -import { Params } from '@angular/router'; +import { convertToParamMap, Params } from '@angular/router'; export class MockActivatedRouteWithSubjects { private subject = new Subject(); params = this.subject; - + snapshot = { paramMap: convertToParamMap({ courseId: '1' }) }; setSubject = (subject: Subject) => { this.params = subject; }; diff --git a/src/test/javascript/spec/integration/code-editor/code-editor-instructor.integration.spec.ts b/src/test/javascript/spec/integration/code-editor/code-editor-instructor.integration.spec.ts index c67a0ed52664..13557d1d71d3 100644 --- a/src/test/javascript/spec/integration/code-editor/code-editor-instructor.integration.spec.ts +++ b/src/test/javascript/spec/integration/code-editor/code-editor-instructor.integration.spec.ts @@ -41,7 +41,7 @@ import { MockParticipationService } from '../../helpers/mocks/service/mock-parti import { MockProgrammingExerciseService } from '../../helpers/mocks/service/mock-programming-exercise.service'; import { WebsocketService } from 'app/core/websocket/websocket.service'; import { MockWebsocketService } from '../../helpers/mocks/service/mock-websocket.service'; -import { MockComponent, MockModule, MockPipe } from 'ng-mocks'; +import { MockComponent, MockModule, MockPipe, MockProvider } from 'ng-mocks'; import { CodeEditorContainerComponent } from 'app/exercises/programming/shared/code-editor/container/code-editor-container.component'; import { IncludedInScoreBadgeComponent } from 'app/exercises/shared/exercise-headers/included-in-score-badge.component'; import { ProgrammingExerciseInstructorExerciseStatusComponent } from 'app/exercises/programming/manage/status/programming-exercise-instructor-exercise-status.component'; @@ -67,6 +67,8 @@ import { CodeEditorMonacoComponent } from 'app/exercises/programming/shared/code import { MarkdownEditorMonacoComponent } from 'app/shared/markdown-editor/monaco/markdown-editor-monaco.component'; import { mockCodeEditorMonacoViewChildren } from '../../helpers/mocks/mock-instance.helper'; import { REPOSITORY } from 'app/exercises/programming/manage/code-editor/code-editor-instructor-base-container.component'; +import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; +import { ProfileInfo } from 'app/shared/layouts/profiles/profile-info.model'; describe('CodeEditorInstructorIntegration', () => { let comp: CodeEditorInstructorAndEditorContainerComponent; @@ -90,6 +92,8 @@ describe('CodeEditorInstructorIntegration', () => { let findWithParticipationsSubject: Subject<{ body: ProgrammingExercise }>; let routeSubject: Subject; + const mockProfileInfo = { activeProfiles: ['iris'] } as ProfileInfo; + // Workaround for an error with MockComponent(). You can remove this once https://github.com/help-me-mom/ng-mocks/issues/8634 is resolved. mockCodeEditorMonacoViewChildren(); @@ -138,6 +142,9 @@ describe('CodeEditorInstructorIntegration', () => { { provide: ProgrammingExerciseParticipationService, useClass: MockProgrammingExerciseParticipationService }, { provide: ProgrammingExerciseService, useClass: MockProgrammingExerciseService }, { provide: WebsocketService, useClass: MockWebsocketService }, + MockProvider(ProfileService, { + getProfileInfo: () => of(mockProfileInfo), + }), ], }) .compileComponents() From 76be7b3d0dfaf27ee4398a67991bac33378a522f Mon Sep 17 00:00:00 2001 From: Michal Kawka <73854755+coolchock@users.noreply.github.com> Date: Thu, 30 Jan 2025 20:46:09 +0100 Subject: [PATCH 3/8] Development: Remove Angular client modules in exam mode (#10169) --- src/main/webapp/app/app.routes.ts | 2 +- .../course/manage/course-management.route.ts | 2 +- .../app/exam/exam-scores/exam-scores.route.ts | 25 --- .../app/exam/manage/exam-management.module.ts | 142 ------------------ .../app/exam/manage/exam-management.route.ts | 18 +-- .../manage/exams/exam-detail.component.ts | 4 +- .../exam-mode-picker.module.ts | 10 -- .../students-upload-images.module.ts | 11 -- .../events/exam-live-events.module.ts | 13 -- .../exam-bar/exam-bar.component.ts | 6 +- .../exam-navigation-bar.module.ts | 12 -- .../exam-participation.component.ts | 10 +- .../participate/exam-participation.module.ts | 73 --------- .../participate/exam-participation.route.ts | 9 -- .../exam-start-information.component.ts | 4 +- ...exam-exercise-update-highlighter.module.ts | 9 -- .../exam-submission-components.module.ts | 46 ------ .../summary/exam-result-summary.component.ts | 2 +- .../summary/exam-result-summary.module.ts | 71 --------- .../participate/timer/exam-timer.module.ts | 10 -- .../app/exam/shared/exam-shared.module.ts | 22 --- .../course-exams/course-exams.module.ts | 13 -- .../app/overview/courses-routing.module.ts | 2 +- .../app/shared/sidebar/sidebar.module.ts | 2 - .../exam-start-information.component.spec.ts | 3 +- 25 files changed, 28 insertions(+), 493 deletions(-) delete mode 100644 src/main/webapp/app/exam/exam-scores/exam-scores.route.ts delete mode 100644 src/main/webapp/app/exam/manage/exam-management.module.ts delete mode 100644 src/main/webapp/app/exam/manage/exams/exam-mode-picker/exam-mode-picker.module.ts delete mode 100644 src/main/webapp/app/exam/manage/students/upload-images/students-upload-images.module.ts delete mode 100644 src/main/webapp/app/exam/participate/events/exam-live-events.module.ts delete mode 100644 src/main/webapp/app/exam/participate/exam-navigation-bar/exam-navigation-bar.module.ts delete mode 100644 src/main/webapp/app/exam/participate/exam-participation.module.ts delete mode 100644 src/main/webapp/app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.module.ts delete mode 100644 src/main/webapp/app/exam/participate/exercises/exam-submission-components.module.ts delete mode 100644 src/main/webapp/app/exam/participate/summary/exam-result-summary.module.ts delete mode 100644 src/main/webapp/app/exam/participate/timer/exam-timer.module.ts delete mode 100644 src/main/webapp/app/exam/shared/exam-shared.module.ts delete mode 100644 src/main/webapp/app/overview/course-exams/course-exams.module.ts diff --git a/src/main/webapp/app/app.routes.ts b/src/main/webapp/app/app.routes.ts index 12e05fc20795..1ee5b6003bfe 100644 --- a/src/main/webapp/app/app.routes.ts +++ b/src/main/webapp/app/app.routes.ts @@ -183,7 +183,7 @@ const routes: Routes = [ // ===== EXAM ===== { path: 'course-management/:courseId/exams', - loadChildren: () => import('./exam/manage/exam-management.module').then((m) => m.ArtemisExamManagementModule), + loadChildren: () => import('./exam/manage/exam-management.route').then((m) => m.examManagementRoute), }, { path: 'courses/:courseId/exams/:examId/grading-system', diff --git a/src/main/webapp/app/course/manage/course-management.route.ts b/src/main/webapp/app/course/manage/course-management.route.ts index eb8a2f8ccf7b..fc55b6daf8c3 100644 --- a/src/main/webapp/app/course/manage/course-management.route.ts +++ b/src/main/webapp/app/course/manage/course-management.route.ts @@ -76,7 +76,7 @@ export const courseManagementState: Routes = [ }, { path: ':courseId/exams', - loadChildren: () => import('../../exam/manage/exam-management.module').then((m) => m.ArtemisExamManagementModule), + loadChildren: () => import('../../exam/manage/exam-management.route').then((m) => m.examManagementRoute), }, { path: ':courseId/tutorial-groups-checklist', diff --git a/src/main/webapp/app/exam/exam-scores/exam-scores.route.ts b/src/main/webapp/app/exam/exam-scores/exam-scores.route.ts deleted file mode 100644 index ac35ee9c1370..000000000000 --- a/src/main/webapp/app/exam/exam-scores/exam-scores.route.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Route, Routes } from '@angular/router'; - -import { Authority } from 'app/shared/constants/authority.constants'; -import { UserRouteAccessService } from 'app/core/auth/user-route-access-service'; - -export const examScoresRoute: Route[] = [ - { - path: ':examId/scores', - loadComponent: () => import('app/exam/exam-scores/exam-scores.component').then((m) => m.ExamScoresComponent), - }, -]; - -const EXAM_SCORES_ROUTES = [...examScoresRoute]; - -export const examScoresState: Routes = [ - { - path: '', - children: EXAM_SCORES_ROUTES, - data: { - authorities: [Authority.ADMIN, Authority.INSTRUCTOR], - pageTitle: 'artemisApp.examScores.title', - }, - canActivate: [UserRouteAccessService], - }, -]; diff --git a/src/main/webapp/app/exam/manage/exam-management.module.ts b/src/main/webapp/app/exam/manage/exam-management.module.ts deleted file mode 100644 index 6118a2fd855e..000000000000 --- a/src/main/webapp/app/exam/manage/exam-management.module.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; -import { ExamManagementComponent } from 'app/exam/manage/exam-management.component'; -import { examManagementState } from 'app/exam/manage/exam-management.route'; -import { ExamUpdateComponent } from 'app/exam/manage/exams/exam-update.component'; -import { ExamDetailComponent } from 'app/exam/manage/exams/exam-detail.component'; -import { ExerciseGroupsComponent } from 'app/exam/manage/exercise-groups/exercise-groups.component'; -import { ExerciseGroupUpdateComponent } from 'app/exam/manage/exercise-groups/exercise-group-update.component'; -import { ExamStudentsComponent } from 'app/exam/manage/students/exam-students.component'; -import { StudentExamsComponent } from 'app/exam/manage/student-exams/student-exams.component'; -import { StudentExamDetailComponent } from 'app/exam/manage/student-exams/student-exam-detail.component'; -import { StudentsUploadImagesModule } from 'app/exam/manage/students/upload-images/students-upload-images.module'; -import { ExamStudentsAttendanceCheckComponent } from 'app/exam/manage/students/verify-attendance-check/exam-students-attendance-check.component'; -import { ArtemisTextExerciseModule } from 'app/exercises/text/manage/text-exercise/text-exercise.module'; -import { ArtemisFileUploadExerciseManagementModule } from 'app/exercises/file-upload/manage/file-upload-exercise-management.module'; -import { ArtemisProgrammingExerciseManagementModule } from 'app/exercises/programming/manage/programming-exercise-management.module'; -import { ArtemisQuizManagementModule } from 'app/exercises/quiz/manage/quiz-management.module'; -import { ArtemisSharedModule } from 'app/shared/shared.module'; -import { ArtemisDataTableModule } from 'app/shared/data-table/data-table.module'; -import { NgxDatatableModule } from '@siemens/ngx-datatable'; -import { FormDateTimePickerModule } from 'app/shared/date-time-picker/date-time-picker.module'; -import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; -import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module'; -import { DurationPipe } from 'app/shared/pipes/artemis-duration.pipe'; -import { StudentExamStatusComponent } from 'app/exam/manage/student-exams/student-exam-status/student-exam-status.component'; -import { StudentExamSummaryComponent } from 'app/exam/manage/student-exams/student-exam-summary.component'; -import { ArtemisParticipationSummaryModule } from 'app/exam/participate/summary/exam-result-summary.module'; -import { ExamExerciseRowButtonsComponent } from 'app/exercises/shared/exam-exercise-row-buttons/exam-exercise-row-buttons.component'; -import { ArtemisProgrammingExerciseStatusModule } from 'app/exercises/programming/manage/status/programming-exercise-status.module'; -import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; -import { ExamChecklistComponent } from 'app/exam/manage/exams/exam-checklist-component/exam-checklist.component'; -import { ExamChecklistExerciseGroupTableComponent } from 'app/exam/manage/exams/exam-checklist-component/exam-checklist-exercisegroup-table/exam-checklist-exercisegroup-table.component'; -import { ArtemisTutorParticipationGraphModule } from 'app/shared/dashboards/tutor-participation-graph/tutor-participation-graph.module'; -import { ProgrammingExerciseGroupCellComponent } from './exercise-groups/programming-exercise-cell/programming-exercise-group-cell.component'; -import { FileUploadExerciseGroupCellComponent } from './exercise-groups/file-upload-exercise-cell/file-upload-exercise-group-cell.component'; -import { ModelingExerciseGroupCellComponent } from './exercise-groups/modeling-exercise-cell/modeling-exercise-group-cell.component'; -import { QuizExerciseGroupCellComponent } from './exercise-groups/quiz-exercise-cell/quiz-exercise-group-cell.component'; -import { ArtemisTextSubmissionAssessmentModule } from 'app/exercises/text/assess/text-submission-assessment.module'; -import { StudentExamDetailTableRowComponent } from 'app/exam/manage/student-exams/student-exam-detail-table-row/student-exam-detail-table-row.component'; -import { ExampleSubmissionsModule } from 'app/exercises/shared/example-submission/example-submissions.module'; -import { BarChartModule } from '@swimlane/ngx-charts'; -import { UserImportModule } from 'app/shared/user-import/user-import.module'; -import { ArtemisExamSharedModule } from 'app/exam/shared/exam-shared.module'; -import { ExamStatusComponent } from 'app/exam/manage/exam-status.component'; -import { ArtemisExamModePickerModule } from 'app/exam/manage/exams/exam-mode-picker/exam-mode-picker.module'; -import { ExamImportComponent } from 'app/exam/manage/exams/exam-import/exam-import.component'; -import { ArtemisHeaderExercisePageWithDetailsModule } from 'app/exercises/shared/exercise-headers/exercise-headers.module'; -import { ExamExerciseImportComponent } from 'app/exam/manage/exams/exam-exercise-import/exam-exercise-import.component'; - -import { BonusComponent } from 'app/grading-system/bonus/bonus.component'; -import { StudentExamTimelineComponent } from './student-exams/student-exam-timeline/student-exam-timeline.component'; -import { TitleChannelNameModule } from 'app/shared/form/title-channel-name/title-channel-name.module'; -import { ExamEditWorkingTimeDialogComponent } from 'app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time-dialog.component'; -import { ExamEditWorkingTimeComponent } from 'app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time.component'; -import { ExamLiveAnnouncementCreateModalComponent } from 'app/exam/manage/exams/exam-checklist-component/exam-announcement-dialog/exam-live-announcement-create-modal.component'; -import { ExamLiveAnnouncementCreateButtonComponent } from 'app/exam/manage/exams/exam-checklist-component/exam-announcement-dialog/exam-live-announcement-create-button.component'; - -import { ArtemisExamNavigationBarModule } from 'app/exam/participate/exam-navigation-bar/exam-navigation-bar.module'; -import { ArtemisExamSubmissionComponentsModule } from 'app/exam/participate/exercises/exam-submission-components.module'; -import { MatSliderModule } from '@angular/material/slider'; -import { ProgrammingExerciseExamDiffComponent } from './student-exams/student-exam-timeline/programming-exam-diff/programming-exercise-exam-diff.component'; -import { ArtemisProgrammingExerciseModule } from 'app/exercises/programming/shared/programming-exercise.module'; -import { DetailModule } from 'app/detail-overview-list/detail.module'; -import { ArtemisDurationFromSecondsPipe } from 'app/shared/pipes/artemis-duration-from-seconds.pipe'; -import { NoDataComponent } from 'app/shared/no-data-component'; -import { SafeHtmlPipe } from 'app/shared/pipes/safe-html.pipe'; -import { GradeStepBoundsPipe } from 'app/shared/pipes/grade-step-bounds.pipe'; -import { examScoresState } from 'app/exam/exam-scores/exam-scores.route'; -import { GitDiffLineStatComponent } from 'app/exercises/programming/git-diff-report/git-diff-line-stat.component'; - -const ENTITY_STATES = [...examManagementState, ...examScoresState]; - -@NgModule({ - // TODO: For better modularization we could define an exercise module with the corresponding exam routes - providers: [ArtemisDurationFromSecondsPipe, SafeHtmlPipe, GradeStepBoundsPipe], - imports: [ - RouterModule.forChild(ENTITY_STATES), - ArtemisTextExerciseModule, - ArtemisSharedModule, - FormDateTimePickerModule, - ArtemisSharedComponentModule, - ArtemisMarkdownEditorModule, - NgxDatatableModule, - ArtemisDataTableModule, - ArtemisFileUploadExerciseManagementModule, - ArtemisProgrammingExerciseManagementModule, - ArtemisQuizManagementModule, - ArtemisParticipationSummaryModule, - ArtemisProgrammingExerciseStatusModule, - ArtemisMarkdownModule, - ArtemisTutorParticipationGraphModule, - ArtemisTextSubmissionAssessmentModule, - ExampleSubmissionsModule, - UserImportModule, - ArtemisExamSharedModule, - ArtemisExamModePickerModule, - ArtemisHeaderExercisePageWithDetailsModule, - BarChartModule, - StudentsUploadImagesModule, - TitleChannelNameModule, - ArtemisExamNavigationBarModule, - ArtemisExamSubmissionComponentsModule, - MatSliderModule, - ArtemisProgrammingExerciseModule, - DetailModule, - NoDataComponent, - GitDiffLineStatComponent, - SafeHtmlPipe, - GradeStepBoundsPipe, - BonusComponent, - ExamManagementComponent, - ExamUpdateComponent, - ExamDetailComponent, - ExerciseGroupsComponent, - ExerciseGroupUpdateComponent, - ExamExerciseRowButtonsComponent, - ExamStudentsComponent, - ExamStudentsAttendanceCheckComponent, - StudentExamStatusComponent, - StudentExamsComponent, - StudentExamDetailComponent, - DurationPipe, - StudentExamSummaryComponent, - ExamChecklistComponent, - ExamChecklistExerciseGroupTableComponent, - ExamStatusComponent, - ProgrammingExerciseGroupCellComponent, - FileUploadExerciseGroupCellComponent, - ModelingExerciseGroupCellComponent, - QuizExerciseGroupCellComponent, - StudentExamDetailTableRowComponent, - ExamImportComponent, - ExamExerciseImportComponent, - ExamEditWorkingTimeComponent, - ExamEditWorkingTimeDialogComponent, - ExamLiveAnnouncementCreateModalComponent, - ExamLiveAnnouncementCreateButtonComponent, - StudentExamTimelineComponent, - ProgrammingExerciseExamDiffComponent, - ], -}) -export class ArtemisExamManagementModule {} diff --git a/src/main/webapp/app/exam/manage/exam-management.route.ts b/src/main/webapp/app/exam/manage/exam-management.route.ts index 6b8cd746e9ae..47113ae86d0f 100644 --- a/src/main/webapp/app/exam/manage/exam-management.route.ts +++ b/src/main/webapp/app/exam/manage/exam-management.route.ts @@ -63,6 +63,15 @@ export const examManagementRoute: Routes = [ }, canActivate: [UserRouteAccessService], }, + { + path: ':examId/scores', + loadComponent: () => import('app/exam/exam-scores/exam-scores.component').then((m) => m.ExamScoresComponent), + data: { + authorities: [Authority.ADMIN, Authority.INSTRUCTOR], + pageTitle: 'artemisApp.examScores.title', + }, + canActivate: [UserRouteAccessService], + }, { path: ':examId', loadComponent: () => import('app/exam/manage/exams/exam-detail.component').then((m) => m.ExamDetailComponent), @@ -996,12 +1005,3 @@ export const examManagementRoute: Routes = [ canActivate: [UserRouteAccessService], }, ]; - -const EXAM_MANAGEMENT_ROUTES = [...examManagementRoute]; - -export const examManagementState: Routes = [ - { - path: '', - children: EXAM_MANAGEMENT_ROUTES, - }, -]; diff --git a/src/main/webapp/app/exam/manage/exams/exam-detail.component.ts b/src/main/webapp/app/exam/manage/exams/exam-detail.component.ts index cbf58cbb97b2..5ad8f10f0e3b 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-detail.component.ts +++ b/src/main/webapp/app/exam/manage/exams/exam-detail.component.ts @@ -14,7 +14,7 @@ import { faAward, faClipboard, faEye, faFlaskVial, faHeartBroken, faListAlt, faT import { AlertService } from 'app/core/util/alert.service'; import { GradingSystemService } from 'app/grading-system/grading-system.service'; import { GradeType } from 'app/entities/grading-scale.model'; -import { DetailOverviewSection, DetailType } from 'app/detail-overview-list/detail-overview-list.component'; +import { DetailOverviewListComponent, DetailOverviewSection, DetailType } from 'app/detail-overview-list/detail-overview-list.component'; import { ArtemisDurationFromSecondsPipe } from 'app/shared/pipes/artemis-duration-from-seconds.pipe'; import { scrollToTopOfPage } from 'app/shared/util/utils'; import { ExerciseType } from 'app/entities/exercise.model'; @@ -23,12 +23,12 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { DeleteButtonDirective } from 'app/shared/delete-dialog/delete-button.directive'; import { CourseExamArchiveButtonComponent } from 'app/shared/components/course-exam-archive-button/course-exam-archive-button.component'; import { ExamChecklistComponent } from './exam-checklist-component/exam-checklist.component'; -import { DetailOverviewListComponent } from 'app/detail-overview-list/detail-overview-list.component'; @Component({ selector: 'jhi-exam-detail', templateUrl: './exam-detail.component.html', imports: [TranslateDirective, RouterLink, FaIconComponent, DeleteButtonDirective, CourseExamArchiveButtonComponent, ExamChecklistComponent, DetailOverviewListComponent], + providers: [ArtemisDurationFromSecondsPipe], }) export class ExamDetailComponent implements OnInit, OnDestroy { private route = inject(ActivatedRoute); diff --git a/src/main/webapp/app/exam/manage/exams/exam-mode-picker/exam-mode-picker.module.ts b/src/main/webapp/app/exam/manage/exams/exam-mode-picker/exam-mode-picker.module.ts deleted file mode 100644 index 50ab25c83469..000000000000 --- a/src/main/webapp/app/exam/manage/exams/exam-mode-picker/exam-mode-picker.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { NgModule } from '@angular/core'; - -import { ExamModePickerComponent } from 'app/exam/manage/exams/exam-mode-picker/exam-mode-picker.component'; -import { ArtemisSharedModule } from 'app/shared/shared.module'; - -@NgModule({ - imports: [ArtemisSharedModule, ExamModePickerComponent], - exports: [ExamModePickerComponent], -}) -export class ArtemisExamModePickerModule {} diff --git a/src/main/webapp/app/exam/manage/students/upload-images/students-upload-images.module.ts b/src/main/webapp/app/exam/manage/students/upload-images/students-upload-images.module.ts deleted file mode 100644 index cbad103c8eef..000000000000 --- a/src/main/webapp/app/exam/manage/students/upload-images/students-upload-images.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { StudentsUploadImagesButtonComponent } from 'app/exam/manage/students/upload-images/students-upload-images-button.component'; -import { StudentsUploadImagesDialogComponent } from 'app/exam/manage/students/upload-images/students-upload-images-dialog.component'; -import { NgModule } from '@angular/core'; -import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; -import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; - -@NgModule({ - imports: [ArtemisSharedComponentModule, ArtemisSharedCommonModule, StudentsUploadImagesDialogComponent, StudentsUploadImagesButtonComponent], - exports: [StudentsUploadImagesDialogComponent, StudentsUploadImagesButtonComponent], -}) -export class StudentsUploadImagesModule {} diff --git a/src/main/webapp/app/exam/participate/events/exam-live-events.module.ts b/src/main/webapp/app/exam/participate/events/exam-live-events.module.ts deleted file mode 100644 index cc805d9c46f0..000000000000 --- a/src/main/webapp/app/exam/participate/events/exam-live-events.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; -import { ArtemisExamTimerModule } from 'app/exam/participate/timer/exam-timer.module'; -import { ExamLiveEventsButtonComponent } from 'app/exam/participate/events/exam-live-events-button.component'; -import { ExamLiveEventsOverlayComponent } from 'app/exam/participate/events/exam-live-events-overlay.component'; -import { ArtemisExamSharedModule } from 'app/exam/shared/exam-shared.module'; - -@NgModule({ - imports: [CommonModule, ArtemisSharedCommonModule, ArtemisExamTimerModule, ArtemisExamSharedModule, ExamLiveEventsButtonComponent, ExamLiveEventsOverlayComponent], - exports: [ExamLiveEventsButtonComponent, ExamLiveEventsOverlayComponent], -}) -export class ArtemisExamLiveEventsModule {} 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 e4c0b2afacf5..b2846149bacb 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 @@ -1,18 +1,18 @@ import { AfterViewInit, Component, ElementRef, EventEmitter, Input, OnInit, Output, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; -import { ArtemisExamTimerModule } from 'app/exam/participate/timer/exam-timer.module'; -import { ArtemisExamLiveEventsModule } from 'app/exam/participate/events/exam-live-events.module'; import { ExamParticipationService } from 'app/exam/participate/exam-participation.service'; import { Exercise, ExerciseType } from 'app/entities/exercise.model'; import { faDoorClosed } from '@fortawesome/free-solid-svg-icons'; import dayjs from 'dayjs/esm'; import { Exam } from 'app/entities/exam/exam.model'; 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'; @Component({ selector: 'jhi-exam-bar', - imports: [CommonModule, ArtemisSharedCommonModule, ArtemisExamTimerModule, ArtemisExamLiveEventsModule], + imports: [CommonModule, ArtemisSharedCommonModule, ExamTimerComponent, ExamLiveEventsButtonComponent], templateUrl: './exam-bar.component.html', styleUrl: './exam-bar.component.scss', }) diff --git a/src/main/webapp/app/exam/participate/exam-navigation-bar/exam-navigation-bar.module.ts b/src/main/webapp/app/exam/participate/exam-navigation-bar/exam-navigation-bar.module.ts deleted file mode 100644 index e56eeac54ab9..000000000000 --- a/src/main/webapp/app/exam/participate/exam-navigation-bar/exam-navigation-bar.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { ExamNavigationBarComponent } from 'app/exam/participate/exam-navigation-bar/exam-navigation-bar.component'; -import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; -import { ArtemisExamTimerModule } from 'app/exam/participate/timer/exam-timer.module'; -import { ArtemisExamLiveEventsModule } from 'app/exam/participate/events/exam-live-events.module'; - -@NgModule({ - imports: [CommonModule, ArtemisSharedCommonModule, ArtemisExamTimerModule, ArtemisExamLiveEventsModule, ExamNavigationBarComponent], - exports: [ExamNavigationBarComponent], -}) -export class ArtemisExamNavigationBarModule {} diff --git a/src/main/webapp/app/exam/participate/exam-participation.component.ts b/src/main/webapp/app/exam/participate/exam-participation.component.ts index 69dde679bdbb..60601da9796f 100644 --- a/src/main/webapp/app/exam/participate/exam-participation.component.ts +++ b/src/main/webapp/app/exam/participate/exam-participation.component.ts @@ -15,7 +15,7 @@ import { Submission } from 'app/entities/submission.model'; import { Exam } from 'app/entities/exam/exam.model'; import { ArtemisServerDateService } from 'app/shared/server-date.service'; import { StudentParticipation } from 'app/entities/participation/student-participation.model'; -import { BehaviorSubject, Observable, Subject, Subscription, of, throwError } from 'rxjs'; +import { BehaviorSubject, Observable, Subject, Subscription, combineLatest, of, throwError } from 'rxjs'; import { catchError, distinctUntilChanged, filter, map, tap, throttleTime, timeout } from 'rxjs/operators'; import { InitializationState } from 'app/entities/participation/participation.model'; import { ProgrammingExercise } from 'app/entities/programming/programming-exercise.model'; @@ -210,8 +210,12 @@ export class ExamParticipationComponent implements OnInit, OnDestroy, ComponentC * loads the exam from the server and initializes the view */ ngOnInit(): void { - this.route.parent?.parent?.params.subscribe((params) => { - this.courseId = parseInt(params['courseId'], 10); + combineLatest({ + parentParams: this.route.parent?.parent?.params ?? of({ courseId: undefined }), + currentParams: this.route.params, + }).subscribe(({ parentParams, currentParams }) => { + const courseId = currentParams['courseId'] || parentParams['courseId']; + this.courseId = parseInt(courseId, 10); }); this.route.params.subscribe((params) => { this.examId = parseInt(params['examId'], 10); diff --git a/src/main/webapp/app/exam/participate/exam-participation.module.ts b/src/main/webapp/app/exam/participate/exam-participation.module.ts deleted file mode 100644 index 980a151486e4..000000000000 --- a/src/main/webapp/app/exam/participate/exam-participation.module.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; - -import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; -import { ExamParticipationComponent } from 'app/exam/participate/exam-participation.component'; -import { ExamParticipationCoverComponent } from './exam-cover/exam-participation-cover.component'; -import { examParticipationState } from 'app/exam/participate/exam-participation.route'; -import { ArtemisQuizQuestionTypesModule } from 'app/exercises/quiz/shared/questions/artemis-quiz-question-types.module'; -import { ArtemisSharedModule } from 'app/shared/shared.module'; -import { ArtemisModelingEditorModule } from 'app/exercises/modeling/shared/modeling-editor.module'; -import { ArtemisFullscreenModule } from 'app/shared/fullscreen/fullscreen.module'; -import { ArtemisProgrammingAssessmentModule } from 'app/exercises/programming/assess/programming-assessment.module'; -import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; -import { ArtemisProgrammingParticipationModule } from 'app/exercises/programming/participate/programming-participation.module'; -import { ArtemisCodeEditorModule } from 'app/exercises/programming/shared/code-editor/code-editor.module'; -import { ArtemisResultModule } from 'app/exercises/shared/result/result.module'; -import { ArtemisProgrammingExerciseActionsModule } from 'app/exercises/programming/shared/actions/programming-exercise-actions.module'; -import { ArtemisParticipationSummaryModule } from 'app/exam/participate/summary/exam-result-summary.module'; -import { ArtemisExerciseButtonsModule } from 'app/overview/exercise-details/exercise-buttons.module'; -import { ArtemisHeaderExercisePageWithDetailsModule } from 'app/exercises/shared/exercise-headers/exercise-headers.module'; -import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; -import { ExamExerciseOverviewPageComponent } from 'app/exam/participate/exercises/exercise-overview-page/exam-exercise-overview-page.component'; -import { SubmissionResultStatusModule } from 'app/overview/submission-result-status.module'; -import { ArtemisExamNavigationBarModule } from 'app/exam/participate/exam-navigation-bar/exam-navigation-bar.module'; -import { ArtemisExamTimerModule } from 'app/exam/participate/timer/exam-timer.module'; -import { ArtemisExamSubmissionComponentsModule } from 'app/exam/participate/exercises/exam-submission-components.module'; -import { ExamExerciseUpdateHighlighterModule } from 'app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.module'; -import { ArtemisExamSharedModule } from 'app/exam/shared/exam-shared.module'; -import { ArtemisExamLiveEventsModule } from 'app/exam/participate/events/exam-live-events.module'; -import { ExamStartInformationComponent } from 'app/exam/participate/exam-start-information/exam-start-information.component'; -import { ArtemisSidebarModule } from 'app/shared/sidebar/sidebar.module'; -import { ExamNavigationSidebarComponent } from 'app/exam/participate/exam-navigation-sidebar/exam-navigation-sidebar.component'; -import { ExamBarComponent } from 'app/exam/participate/exam-bar/exam-bar.component'; -import { TestRunRibbonComponent } from 'app/exam/manage/test-runs/test-run-ribbon.component'; - -const ENTITY_STATES = [...examParticipationState]; - -@NgModule({ - imports: [ - RouterModule.forChild(ENTITY_STATES), - ArtemisSharedCommonModule, - ArtemisHeaderExercisePageWithDetailsModule, - ArtemisSharedModule, - ArtemisModelingEditorModule, - ArtemisQuizQuestionTypesModule, - ArtemisFullscreenModule, - ArtemisSharedComponentModule, - ArtemisProgrammingParticipationModule, - ArtemisCodeEditorModule, - ArtemisResultModule, - ArtemisProgrammingExerciseActionsModule, - ArtemisExerciseButtonsModule, - ArtemisProgrammingAssessmentModule, - ArtemisParticipationSummaryModule, - ArtemisMarkdownModule, - SubmissionResultStatusModule, - ArtemisExamNavigationBarModule, - ArtemisExamTimerModule, - ArtemisExamSubmissionComponentsModule, - ExamExerciseUpdateHighlighterModule, - ArtemisExamSharedModule, - ArtemisExamLiveEventsModule, - ExamStartInformationComponent, - ArtemisSidebarModule, - ExamNavigationSidebarComponent, - ExamBarComponent, - ExamParticipationComponent, - ExamParticipationCoverComponent, - ExamExerciseOverviewPageComponent, - TestRunRibbonComponent, - ], -}) -export class ArtemisExamParticipationModule {} diff --git a/src/main/webapp/app/exam/participate/exam-participation.route.ts b/src/main/webapp/app/exam/participate/exam-participation.route.ts index dfced7d039e0..6debe5bf4e5b 100644 --- a/src/main/webapp/app/exam/participate/exam-participation.route.ts +++ b/src/main/webapp/app/exam/participate/exam-participation.route.ts @@ -53,12 +53,3 @@ export const examParticipationRoute: Routes = [ canActivate: [UserRouteAccessService], }, ]; - -const EXAM_PARTICIPATION_ROUTES = [...examParticipationRoute]; - -export const examParticipationState: Routes = [ - { - path: '', - children: EXAM_PARTICIPATION_ROUTES, - }, -]; diff --git a/src/main/webapp/app/exam/participate/exam-start-information/exam-start-information.component.ts b/src/main/webapp/app/exam/participate/exam-start-information/exam-start-information.component.ts index 6e56677edc85..b0539ddd97c3 100644 --- a/src/main/webapp/app/exam/participate/exam-start-information/exam-start-information.component.ts +++ b/src/main/webapp/app/exam/participate/exam-start-information/exam-start-information.component.ts @@ -4,13 +4,13 @@ import { ArtemisSharedComponentModule } from 'app/shared/components/shared-compo import { InformationBox, InformationBoxComponent, InformationBoxContent } from 'app/shared/information-box/information-box.component'; import { Exam } from 'app/entities/exam/exam.model'; import { StudentExam } from 'app/entities/student-exam.model'; -import { ArtemisExamSharedModule } from 'app/exam/shared/exam-shared.module'; import dayjs from 'dayjs/esm'; import { SafeHtml } from '@angular/platform-browser'; +import { StudentExamWorkingTimeComponent } from 'app/exam/shared/student-exam-working-time/student-exam-working-time.component'; @Component({ selector: 'jhi-exam-start-information', - imports: [ArtemisSharedModule, ArtemisSharedComponentModule, InformationBoxComponent, ArtemisExamSharedModule], + imports: [ArtemisSharedModule, ArtemisSharedComponentModule, InformationBoxComponent, StudentExamWorkingTimeComponent], templateUrl: './exam-start-information.component.html', }) export class ExamStartInformationComponent implements OnInit { diff --git a/src/main/webapp/app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.module.ts b/src/main/webapp/app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.module.ts deleted file mode 100644 index e915682f2001..000000000000 --- a/src/main/webapp/app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { NgModule } from '@angular/core'; -import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; -import { ExamExerciseUpdateHighlighterComponent } from 'app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.component'; - -@NgModule({ - imports: [ArtemisSharedCommonModule, ExamExerciseUpdateHighlighterComponent], - exports: [ExamExerciseUpdateHighlighterComponent], -}) -export class ExamExerciseUpdateHighlighterModule {} diff --git a/src/main/webapp/app/exam/participate/exercises/exam-submission-components.module.ts b/src/main/webapp/app/exam/participate/exercises/exam-submission-components.module.ts deleted file mode 100644 index 5fbbb9aee4bc..000000000000 --- a/src/main/webapp/app/exam/participate/exercises/exam-submission-components.module.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FileUploadExamSubmissionComponent } from 'app/exam/participate/exercises/file-upload/file-upload-exam-submission.component'; -import { QuizExamSubmissionComponent } from 'app/exam/participate/exercises/quiz/quiz-exam-submission.component'; -import { ProgrammingExamSubmissionComponent } from 'app/exam/participate/exercises/programming/programming-exam-submission.component'; -import { TextExamSubmissionComponent } from 'app/exam/participate/exercises/text/text-exam-submission.component'; -import { ModelingExamSubmissionComponent } from 'app/exam/participate/exercises/modeling/modeling-exam-submission.component'; -import { ArtemisSharedModule } from 'app/shared/shared.module'; -import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; -import { ExamExerciseUpdateHighlighterModule } from 'app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.module'; -import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; -import { ArtemisQuizQuestionTypesModule } from 'app/exercises/quiz/shared/questions/artemis-quiz-question-types.module'; -import { SubmissionResultStatusModule } from 'app/overview/submission-result-status.module'; -import { ArtemisExerciseButtonsModule } from 'app/overview/exercise-details/exercise-buttons.module'; -import { ArtemisProgrammingExerciseActionsModule } from 'app/exercises/programming/shared/actions/programming-exercise-actions.module'; -import { ArtemisCodeEditorModule } from 'app/exercises/programming/shared/code-editor/code-editor.module'; -import { ArtemisFullscreenModule } from 'app/shared/fullscreen/fullscreen.module'; -import { ArtemisModelingEditorModule } from 'app/exercises/modeling/shared/modeling-editor.module'; -import { ArtemisProgrammingSubmissionPolicyStatusModule } from 'app/exercises/programming/participate/programming-submission-policy-status.module'; -import { ExerciseSaveButtonComponent } from './exercise-save-button/exercise-save-button.component'; - -@NgModule({ - imports: [ - CommonModule, - ArtemisSharedModule, - ArtemisMarkdownModule, - ArtemisSharedComponentModule, - ArtemisQuizQuestionTypesModule, - SubmissionResultStatusModule, - ArtemisExerciseButtonsModule, - ArtemisProgrammingExerciseActionsModule, - ArtemisCodeEditorModule, - ArtemisFullscreenModule, - ArtemisModelingEditorModule, - ArtemisProgrammingSubmissionPolicyStatusModule, - ExamExerciseUpdateHighlighterModule, - ExerciseSaveButtonComponent, - FileUploadExamSubmissionComponent, - QuizExamSubmissionComponent, - ProgrammingExamSubmissionComponent, - TextExamSubmissionComponent, - ModelingExamSubmissionComponent, - ], - exports: [FileUploadExamSubmissionComponent, QuizExamSubmissionComponent, ProgrammingExamSubmissionComponent, TextExamSubmissionComponent, ModelingExamSubmissionComponent], -}) -export class ArtemisExamSubmissionComponentsModule {} diff --git a/src/main/webapp/app/exam/participate/summary/exam-result-summary.component.ts b/src/main/webapp/app/exam/participate/summary/exam-result-summary.component.ts index 3f2988b91922..f50d7b278a6e 100644 --- a/src/main/webapp/app/exam/participate/summary/exam-result-summary.component.ts +++ b/src/main/webapp/app/exam/participate/summary/exam-result-summary.component.ts @@ -185,7 +185,7 @@ export class ExamResultSummaryComponent implements OnInit { this.isTestExam = this.studentExam.exam!.testExam!; this.testRunConduction = this.isTestRun && this.route.snapshot.url[3]?.toString() === 'conduction'; this.testExamConduction = this.isTestExam && !this.studentExam.submitted; - this.courseId = Number(this.route.parent?.parent?.snapshot.paramMap.get('courseId')); + this.courseId = Number(this.route.snapshot?.paramMap?.get('courseId') || this.route.parent?.parent?.snapshot.paramMap.get('courseId')); if (!this.studentExam?.id) { throw new Error('studentExam.id should be present to fetch grade info'); } diff --git a/src/main/webapp/app/exam/participate/summary/exam-result-summary.module.ts b/src/main/webapp/app/exam/participate/summary/exam-result-summary.module.ts deleted file mode 100644 index f9c47c383be0..000000000000 --- a/src/main/webapp/app/exam/participate/summary/exam-result-summary.module.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; -import { ExamResultSummaryComponent } from 'app/exam/participate/summary/exam-result-summary.component'; -import { ProgrammingExamSummaryComponent } from 'app/exam/participate/summary/exercises/programming-exam-summary/programming-exam-summary.component'; -import { ModelingExamSummaryComponent } from 'app/exam/participate/summary/exercises/modeling-exam-summary/modeling-exam-summary.component'; -import { FileUploadExamSummaryComponent } from 'app/exam/participate/summary/exercises/file-upload-exam-summary/file-upload-exam-summary.component'; -import { TextExamSummaryComponent } from 'app/exam/participate/summary/exercises/text-exam-summary/text-exam-summary.component'; -import { QuizExamSummaryComponent } from 'app/exam/participate/summary/exercises/quiz-exam-summary/quiz-exam-summary.component'; -import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; -import { ArtemisSharedModule } from 'app/shared/shared.module'; -import { ArtemisQuizQuestionTypesModule } from 'app/exercises/quiz/shared/questions/artemis-quiz-question-types.module'; -import { ArtemisModelingEditorModule } from 'app/exercises/modeling/shared/modeling-editor.module'; -import { ArtemisFullscreenModule } from 'app/shared/fullscreen/fullscreen.module'; -import { ArtemisResultModule } from 'app/exercises/shared/result/result.module'; -import { ArtemisComplaintsModule } from 'app/complaints/complaints.module'; -import { ExamGeneralInformationComponent } from 'app/exam/participate/general-information/exam-general-information.component'; -import { ExamResultOverviewComponent } from 'app/exam/participate/summary/result-overview/exam-result-overview.component'; -import { ArtemisHeaderExercisePageWithDetailsModule } from 'app/exercises/shared/exercise-headers/exercise-headers.module'; -import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; -import { SubmissionResultStatusModule } from 'app/overview/submission-result-status.module'; -import { ArtemisExamSharedModule } from 'app/exam/shared/exam-shared.module'; -import { ExampleSolutionComponent } from 'app/exercises/shared/example-solution/example-solution.component'; -import { ArtemisProgrammingExerciseManagementModule } from 'app/exercises/programming/manage/programming-exercise-management.module'; -import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; -import { GradingKeyOverviewModule } from 'app/grading-system/grading-key-overview/grading-key-overview.module'; -import { ExamResultSummaryExerciseCardHeaderComponent } from 'app/exam/participate/summary/exercises/header/exam-result-summary-exercise-card-header.component'; -import { ArtemisModelingParticipationModule } from 'app/exercises/modeling/participate/modeling-participation.module'; -import { ArtemisTextParticipationModule } from 'app/exercises/text/participate/text-participation.module'; -import { ArtemisFileUploadParticipationModule } from 'app/exercises/file-upload/participate/file-upload-participation.module'; -import { ArtemisFeedbackModule } from 'app/exercises/shared/feedback/feedback.module'; -import { CollapsibleCardComponent } from 'app/exam/participate/summary/collapsible-card.component'; -import { NoDataComponent } from 'app/shared/no-data-component'; - -@NgModule({ - imports: [ - RouterModule, - ArtemisSharedCommonModule, - ArtemisSharedModule, - ArtemisSharedComponentModule, - ArtemisQuizQuestionTypesModule, - ArtemisModelingEditorModule, - ArtemisFullscreenModule, - ArtemisResultModule, - ArtemisComplaintsModule, - ArtemisProgrammingExerciseManagementModule, - ArtemisHeaderExercisePageWithDetailsModule, - ArtemisMarkdownModule, - SubmissionResultStatusModule, - ArtemisExamSharedModule, - ArtemisSharedComponentModule, - GradingKeyOverviewModule, - ArtemisModelingParticipationModule, - ArtemisTextParticipationModule, - ArtemisFileUploadParticipationModule, - ArtemisFeedbackModule, - NoDataComponent, - ExamResultSummaryComponent, - ProgrammingExamSummaryComponent, - ModelingExamSummaryComponent, - FileUploadExamSummaryComponent, - TextExamSummaryComponent, - QuizExamSummaryComponent, - ExamGeneralInformationComponent, - ExamResultOverviewComponent, - ExamResultSummaryExerciseCardHeaderComponent, - ExampleSolutionComponent, - CollapsibleCardComponent, - ], - exports: [ExamResultSummaryComponent, ExamGeneralInformationComponent], -}) -export class ArtemisParticipationSummaryModule {} diff --git a/src/main/webapp/app/exam/participate/timer/exam-timer.module.ts b/src/main/webapp/app/exam/participate/timer/exam-timer.module.ts deleted file mode 100644 index 94c53564d044..000000000000 --- a/src/main/webapp/app/exam/participate/timer/exam-timer.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { ExamTimerComponent } from 'app/exam/participate/timer/exam-timer.component'; -import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; - -@NgModule({ - imports: [CommonModule, ArtemisSharedCommonModule, ExamTimerComponent], - exports: [ExamTimerComponent], -}) -export class ArtemisExamTimerModule {} diff --git a/src/main/webapp/app/exam/shared/exam-shared.module.ts b/src/main/webapp/app/exam/shared/exam-shared.module.ts deleted file mode 100644 index 68d31244d222..000000000000 --- a/src/main/webapp/app/exam/shared/exam-shared.module.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { NgModule } from '@angular/core'; -import { StudentExamWorkingTimeComponent } from 'app/exam/shared/student-exam-working-time/student-exam-working-time.component'; -import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; -import { TestExamWorkingTimeComponent } from 'app/exam/shared/testExam-workingTime/test-exam-working-time.component'; -import { WorkingTimeControlComponent } from 'app/exam/shared/working-time-control/working-time-control.component'; -import { ExamLiveEventComponent } from 'app/exam/shared/events/exam-live-event.component'; -import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; -import { WorkingTimeChangeComponent } from 'app/exam/shared/working-time-change/working-time-change.component'; - -@NgModule({ - imports: [ - ArtemisSharedCommonModule, - ArtemisMarkdownModule, - StudentExamWorkingTimeComponent, - TestExamWorkingTimeComponent, - WorkingTimeControlComponent, - WorkingTimeChangeComponent, - ExamLiveEventComponent, - ], - exports: [StudentExamWorkingTimeComponent, TestExamWorkingTimeComponent, WorkingTimeControlComponent, WorkingTimeChangeComponent, ExamLiveEventComponent], -}) -export class ArtemisExamSharedModule {} diff --git a/src/main/webapp/app/overview/course-exams/course-exams.module.ts b/src/main/webapp/app/overview/course-exams/course-exams.module.ts deleted file mode 100644 index 69d09384d273..000000000000 --- a/src/main/webapp/app/overview/course-exams/course-exams.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ArtemisSharedModule } from 'app/shared/shared.module'; -import { CourseExamsComponent } from 'app/overview/course-exams/course-exams.component'; -import { ArtemisExamSharedModule } from 'app/exam/shared/exam-shared.module'; -import { NgModule } from '@angular/core'; -import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; -import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; -import { ArtemisSidebarModule } from 'app/shared/sidebar/sidebar.module'; - -@NgModule({ - imports: [ArtemisSharedModule, ArtemisSharedCommonModule, ArtemisSharedComponentModule, ArtemisExamSharedModule, ArtemisSidebarModule, CourseExamsComponent], - exports: [CourseExamsComponent], -}) -export class CourseExamsModule {} diff --git a/src/main/webapp/app/overview/courses-routing.module.ts b/src/main/webapp/app/overview/courses-routing.module.ts index 0669891b2db4..79171c01970b 100644 --- a/src/main/webapp/app/overview/courses-routing.module.ts +++ b/src/main/webapp/app/overview/courses-routing.module.ts @@ -269,7 +269,7 @@ const routes: Routes = [ }, canActivate: [UserRouteAccessService], canDeactivate: [PendingChangesGuard], - loadChildren: () => import('../exam/participate/exam-participation.module').then((m) => m.ArtemisExamParticipationModule), + loadChildren: () => import('../exam/participate/exam-participation.route').then((m) => m.examParticipationRoute), }, ], }, diff --git a/src/main/webapp/app/shared/sidebar/sidebar.module.ts b/src/main/webapp/app/shared/sidebar/sidebar.module.ts index 8f46f0f921c3..24a81c5ff50f 100644 --- a/src/main/webapp/app/shared/sidebar/sidebar.module.ts +++ b/src/main/webapp/app/shared/sidebar/sidebar.module.ts @@ -13,7 +13,6 @@ import { ArtemisSharedCommonModule } from '../shared-common.module'; import { SubmissionResultStatusModule } from 'app/overview/submission-result-status.module'; import { SidebarCardDirective } from 'app/shared/sidebar/sidebar-card.directive'; import { ConversationOptionsComponent } from 'app/shared/sidebar/conversation-options/conversation-options.component'; -import { ArtemisExamSharedModule } from 'app/exam/shared/exam-shared.module'; import { SearchFilterComponent } from 'app/shared/search-filter/search-filter.component'; import { ProfilePictureComponent } from 'app/shared/profile-picture/profile-picture.component'; @@ -26,7 +25,6 @@ import { ProfilePictureComponent } from 'app/shared/profile-picture/profile-pict ArtemisSharedCommonModule, SubmissionResultStatusModule, SidebarCardDirective, - ArtemisExamSharedModule, SearchFilterComponent, ProfilePictureComponent, SidebarAccordionComponent, diff --git a/src/test/javascript/spec/component/exam/participate/exam-start-information/exam-start-information.component.spec.ts b/src/test/javascript/spec/component/exam/participate/exam-start-information/exam-start-information.component.spec.ts index c675ea69d8c7..6cf5e3ef6db6 100644 --- a/src/test/javascript/spec/component/exam/participate/exam-start-information/exam-start-information.component.spec.ts +++ b/src/test/javascript/spec/component/exam/participate/exam-start-information/exam-start-information.component.spec.ts @@ -12,7 +12,6 @@ import dayjs from 'dayjs/esm'; import { MockComponent, MockDirective, MockPipe } from 'ng-mocks'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; -import { ArtemisExamSharedModule } from 'app/exam/shared/exam-shared.module'; import { TranslateDirective } from 'app/shared/language/translate.directive'; import { provideRouter } from '@angular/router'; @@ -40,7 +39,7 @@ describe('ExamStartInformationComponent', () => { studentExam = { id: 1, exam, user, workingTime: 60, submitted: true } as StudentExam; return TestBed.configureTestingModule({ - imports: [ArtemisSharedModule, ArtemisSharedComponentModule, ArtemisExamSharedModule], + imports: [ArtemisSharedModule, ArtemisSharedComponentModule], declarations: [ ExamStartInformationComponent, MockComponent(StudentExamWorkingTimeComponent), From 1179759e47ee10a8327a4348be754e2843cac49e Mon Sep 17 00:00:00 2001 From: Ole Vester <73833780+ole-ve@users.noreply.github.com> Date: Thu, 30 Jan 2025 20:58:41 +0100 Subject: [PATCH 4/8] Development: Run GitHub coverage summary step only on success in default test pipeline (#10186) --- .github/workflows/test.yml | 2 +- gradle/jacoco.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ba23a4d736bc..4442571b230c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -119,7 +119,7 @@ jobs: name: Coverage Report Server Tests path: build/reports/jacoco/test/html - name: Append Per-Module Coverage to Job Summary - if: success() || failure() + if: success() run: | AGGREGATED_REPORT_FILE=./module_coverage_report.md python3 ./supporting_scripts/code-coverage/per_module_cov_report/parse_module_coverage.py build/reports/jacoco $AGGREGATED_REPORT_FILE diff --git a/gradle/jacoco.gradle b/gradle/jacoco.gradle index ceb43a8d5ec6..a76db0977c6e 100644 --- a/gradle/jacoco.gradle +++ b/gradle/jacoco.gradle @@ -83,10 +83,10 @@ ext { ? ModuleCoverageThresholds.collect {element -> element.key} : includedModules as ArrayList - // we want to ignore some generated files in the domain folders ignoredDirectories = [ "**/$BasePath/**/domain/**/*_*", "**/$BasePath/core/config/migration/entries/**", + "**/org/gradle/**", "**/gradle-wrapper.jar/**" ] } From 623907b1748883b80f12af65f3d8e71bf5211240 Mon Sep 17 00:00:00 2001 From: Marcel Gaupp Date: Thu, 30 Jan 2025 21:05:43 +0100 Subject: [PATCH 5/8] Programming exercises: Add MATLAB programming exercise template (#10039) --- .../programming-exercise-features.inc | 4 ++ .../de/tum/cit/aet/artemis/ArtemisApp.java | 3 +- .../service/BuildJobContainerService.java | 2 +- .../core/config/LicenseConfiguration.java | 30 +++++++++ .../domain/ProgrammingLanguage.java | 1 + .../programming/service/LicenseService.java | 60 ++++++++++++++++++ ...ProgrammingExerciseBuildConfigService.java | 63 ++++++++++++++++--- .../service/ProgrammingExerciseService.java | 2 +- .../ProgrammingLanguageFeatureService.java | 58 ++++++++++++++--- .../service/TemplateUpgradePolicyService.java | 4 +- .../ci/ContinuousIntegrationService.java | 9 +-- ...abCIProgrammingLanguageFeatureService.java | 13 +++- ...kinsProgrammingLanguageFeatureService.java | 13 +++- ...alCIProgrammingLanguageFeatureService.java | 15 ++++- src/main/resources/config/application.yml | 2 + .../templates/aeolus/matlab/default.sh | 20 ++++++ .../templates/aeolus/matlab/default.yaml | 18 ++++++ .../templates/matlab/exercise/.gitattributes | 28 +++++++++ .../templates/matlab/exercise/.gitignore | 36 +++++++++++ .../matlab/exercise/averageGradeByStudent.m | 3 + .../templates/matlab/exercise/finalGrade.m | 3 + .../matlab/exercise/medianGradeByAssignment.m | 3 + src/main/resources/templates/matlab/readme | 49 +++++++++++++++ .../templates/matlab/solution/.gitattributes | 28 +++++++++ .../templates/matlab/solution/.gitignore | 36 +++++++++++ .../matlab/solution/averageGradeByStudent.m | 3 + .../templates/matlab/solution/finalGrade.m | 3 + .../matlab/solution/medianGradeByAssignment.m | 3 + .../templates/matlab/test/.gitattributes | 28 +++++++++ .../templates/matlab/test/.gitignore | 39 ++++++++++++ .../templates/matlab/test/testRunner.m | 12 ++++ .../templates/matlab/test/tests/GradeTest.m | 45 +++++++++++++ .../programming/programming-exercise.model.ts | 1 + .../programming-exercise-detail.component.ts | 7 +-- .../programming-exercise-update.component.ts | 4 +- .../programming-language-feature.service.ts | 4 +- .../core/config/LicenseConfigurationTest.java | 27 ++++++++ .../service/LicenseServiceTest.java | 51 +++++++++++++++ .../programming/util/ArgumentSources.java | 3 +- ...ctSpringIntegrationLocalCILocalVCTest.java | 3 +- src/test/resources/config/application.yml | 7 ++- 41 files changed, 704 insertions(+), 39 deletions(-) create mode 100644 src/main/java/de/tum/cit/aet/artemis/core/config/LicenseConfiguration.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/programming/service/LicenseService.java create mode 100644 src/main/resources/templates/aeolus/matlab/default.sh create mode 100644 src/main/resources/templates/aeolus/matlab/default.yaml create mode 100644 src/main/resources/templates/matlab/exercise/.gitattributes create mode 100644 src/main/resources/templates/matlab/exercise/.gitignore create mode 100644 src/main/resources/templates/matlab/exercise/averageGradeByStudent.m create mode 100644 src/main/resources/templates/matlab/exercise/finalGrade.m create mode 100644 src/main/resources/templates/matlab/exercise/medianGradeByAssignment.m create mode 100644 src/main/resources/templates/matlab/readme create mode 100644 src/main/resources/templates/matlab/solution/.gitattributes create mode 100644 src/main/resources/templates/matlab/solution/.gitignore create mode 100644 src/main/resources/templates/matlab/solution/averageGradeByStudent.m create mode 100644 src/main/resources/templates/matlab/solution/finalGrade.m create mode 100644 src/main/resources/templates/matlab/solution/medianGradeByAssignment.m create mode 100644 src/main/resources/templates/matlab/test/.gitattributes create mode 100644 src/main/resources/templates/matlab/test/.gitignore create mode 100644 src/main/resources/templates/matlab/test/testRunner.m create mode 100644 src/main/resources/templates/matlab/test/tests/GradeTest.m create mode 100644 src/test/java/de/tum/cit/aet/artemis/core/config/LicenseConfigurationTest.java create mode 100644 src/test/java/de/tum/cit/aet/artemis/programming/service/LicenseServiceTest.java diff --git a/docs/user/exercises/programming-exercise-features.inc b/docs/user/exercises/programming-exercise-features.inc index 2c29ff79a1f4..4c3fea5bf56e 100644 --- a/docs/user/exercises/programming-exercise-features.inc +++ b/docs/user/exercises/programming-exercise-features.inc @@ -49,6 +49,8 @@ Instructors can still use those templates to generate programming exercises and +----------------------+----------+---------+ | Bash | yes | yes | +----------------------+----------+---------+ + | MATLAB | yes | no | + +----------------------+----------+---------+ - Not all ``templates`` support the same feature set and supported features can also change depending on the continuous integration system setup. Depending on the feature set, some options might not be available during the creation of the programming exercise. @@ -95,6 +97,8 @@ Instructors can still use those templates to generate programming exercises and +----------------------+----------------------+----------------------+---------------------+--------------+------------------------------------------+------------------------------+------------------------+ | Bash | no | no | no | no | n/a | no | L: yes, J: no | +----------------------+----------------------+----------------------+---------------------+--------------+------------------------------------------+------------------------------+------------------------+ + | MATLAB | no | no | no | no | n/a | no | L: yes, J: no | + +----------------------+----------------------+----------------------+---------------------+--------------+------------------------------------------+------------------------------+------------------------+ - *Sequential Test Runs*: ``Artemis`` can generate a build plan which first executes structural and then behavioral tests. This feature can help students to better concentrate on the immediate challenge at hand. - *Static Code Analysis*: ``Artemis`` can generate a build plan which additionally executes static code analysis tools. diff --git a/src/main/java/de/tum/cit/aet/artemis/ArtemisApp.java b/src/main/java/de/tum/cit/aet/artemis/ArtemisApp.java index 11b1e7c67636..b56b0f82990d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/ArtemisApp.java +++ b/src/main/java/de/tum/cit/aet/artemis/ArtemisApp.java @@ -18,13 +18,14 @@ import org.springframework.boot.info.GitProperties; import org.springframework.core.env.Environment; +import de.tum.cit.aet.artemis.core.config.LicenseConfiguration; import de.tum.cit.aet.artemis.core.config.ProgrammingLanguageConfiguration; import de.tum.cit.aet.artemis.core.config.TheiaConfiguration; import tech.jhipster.config.DefaultProfileUtil; import tech.jhipster.config.JHipsterConstants; @SpringBootApplication -@EnableConfigurationProperties({ LiquibaseProperties.class, ProgrammingLanguageConfiguration.class, TheiaConfiguration.class }) +@EnableConfigurationProperties({ LiquibaseProperties.class, ProgrammingLanguageConfiguration.class, TheiaConfiguration.class, LicenseConfiguration.class }) public class ArtemisApp { private static final Logger log = LoggerFactory.getLogger(ArtemisApp.class); diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobContainerService.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobContainerService.java index 891de1f8af4c..89c137972c66 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobContainerService.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobContainerService.java @@ -108,7 +108,7 @@ public CreateContainerResponse configureContainer(String containerName, String i // container from exiting until it finishes. // It waits until the script that is running the tests (see below execCreateCmdResponse) is completed, and until the result files are extracted which is indicated // by the creation of a file "stop_container.txt" in the container's root directory. - .withCmd("sh", "-c", "while [ ! -f " + LOCALCI_WORKING_DIRECTORY + "/stop_container.txt ]; do sleep 0.5; done") + .withEntrypoint().withCmd("sh", "-c", "while [ ! -f " + LOCALCI_WORKING_DIRECTORY + "/stop_container.txt ]; do sleep 0.5; done") // .withCmd("tail", "-f", "/dev/null") // Activate for debugging purposes instead of the above command to get a running container that you can peek into using // "docker exec -it /bin/bash". .exec(); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/LicenseConfiguration.java b/src/main/java/de/tum/cit/aet/artemis/core/config/LicenseConfiguration.java new file mode 100644 index 000000000000..6ebe6709d2d1 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/LicenseConfiguration.java @@ -0,0 +1,30 @@ +package de.tum.cit.aet.artemis.core.config; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + +import jakarta.annotation.Nullable; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Profile; + +@Profile(PROFILE_CORE) +@ConfigurationProperties(prefix = "artemis.licenses") +public class LicenseConfiguration { + + private final MatLabLicense matlab; + + public record MatLabLicense(String licenseServer) { + } + + public LicenseConfiguration(MatLabLicense matlab) { + this.matlab = matlab; + } + + @Nullable + public String getMatlabLicenseServer() { + if (matlab == null) { + return null; + } + return matlab.licenseServer(); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingLanguage.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingLanguage.java index 0c4894182e1e..fe6a36d2eed9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingLanguage.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingLanguage.java @@ -48,6 +48,7 @@ public enum ProgrammingLanguage { JAVA, JAVASCRIPT, KOTLIN, + MATLAB, OCAML, PYTHON, R, diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/LicenseService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/LicenseService.java new file mode 100644 index 000000000000..4999dccc4d30 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/LicenseService.java @@ -0,0 +1,60 @@ +package de.tum.cit.aet.artemis.programming.service; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + +import java.util.Map; +import java.util.Objects; + +import jakarta.annotation.Nullable; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import de.tum.cit.aet.artemis.core.config.LicenseConfiguration; +import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage; +import de.tum.cit.aet.artemis.programming.domain.ProjectType; + +/** + * Provides licensing information for proprietary software to build jobs via environment variables. + */ +@Profile(PROFILE_CORE) +@Service +public class LicenseService { + + private final LicenseConfiguration licenseConfiguration; + + public LicenseService(LicenseConfiguration licenseConfiguration) { + this.licenseConfiguration = licenseConfiguration; + } + + /** + * Checks whether a required license is configured for the specified exercise type. + * If no license is required this returns true. + * + * @param programmingLanguage the programming language of the exercise type + * @param projectType the project type of the exercise type + * @return whether a required license is configured + */ + public boolean isLicensed(ProgrammingLanguage programmingLanguage, @Nullable ProjectType projectType) { + if (programmingLanguage == ProgrammingLanguage.MATLAB && projectType == null) { + return licenseConfiguration.getMatlabLicenseServer() != null; + } + + return true; + } + + /** + * Returns environment variables required to run programming exercise tests. + * + * @param programmingLanguage the programming language of the exercise + * @param projectType the project type of the exercise + * @return environment variables for the specified exercise type + */ + public Map getEnvironment(ProgrammingLanguage programmingLanguage, @Nullable ProjectType projectType) { + if (programmingLanguage == ProgrammingLanguage.MATLAB && projectType == null) { + return Map.of("MLM_LICENSE_FILE", Objects.requireNonNull(licenseConfiguration.getMatlabLicenseServer())); + } + + return Map.of(); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseBuildConfigService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseBuildConfigService.java index 5ccf7f2045a6..bc8a6a93dc4b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseBuildConfigService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseBuildConfigService.java @@ -3,6 +3,7 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -17,7 +18,10 @@ import de.tum.cit.aet.artemis.buildagent.dto.DockerFlagsDTO; import de.tum.cit.aet.artemis.buildagent.dto.DockerRunConfig; +import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseBuildConfig; +import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage; +import de.tum.cit.aet.artemis.programming.domain.ProjectType; @Profile(PROFILE_CORE) @Service @@ -27,6 +31,12 @@ public class ProgrammingExerciseBuildConfigService { private final ObjectMapper objectMapper = new ObjectMapper(); + private final LicenseService licenseService; + + public ProgrammingExerciseBuildConfigService(LicenseService licenseService) { + this.licenseService = licenseService; + } + /** * Converts a JSON string representing Docker flags (in JSON format) * into a {@link DockerRunConfig} instance. @@ -46,25 +56,60 @@ public class ProgrammingExerciseBuildConfigService { public DockerRunConfig getDockerRunConfig(ProgrammingExerciseBuildConfig buildConfig) { DockerFlagsDTO dockerFlagsDTO = parseDockerFlags(buildConfig); - return getDockerRunConfigFromParsedFlags(dockerFlagsDTO); + String network; + Map exerciseEnvironment; + if (dockerFlagsDTO != null) { + network = dockerFlagsDTO.network(); + exerciseEnvironment = dockerFlagsDTO.env(); + } + else { + network = null; + exerciseEnvironment = null; + } + + ProgrammingExercise exercise = buildConfig.getProgrammingExercise(); + if (exercise == null) { + return createDockerRunConfig(network, exerciseEnvironment); + } + + ProgrammingLanguage programmingLanguage = exercise.getProgrammingLanguage(); + ProjectType projectType = exercise.getProjectType(); + Map environment = addLanguageSpecificEnvironment(exerciseEnvironment, programmingLanguage, projectType); + + return createDockerRunConfig(network, environment); + } + + @Nullable + private Map addLanguageSpecificEnvironment(@Nullable Map exerciseEnvironment, ProgrammingLanguage language, ProjectType projectType) { + Map licenseEnvironment = licenseService.getEnvironment(language, projectType); + if (licenseEnvironment.isEmpty()) { + return exerciseEnvironment; + } + + Map env = new HashMap<>(licenseEnvironment); + if (exerciseEnvironment != null) { + env.putAll(exerciseEnvironment); + } + + return env; } - DockerRunConfig getDockerRunConfigFromParsedFlags(DockerFlagsDTO dockerFlagsDTO) { - if (dockerFlagsDTO == null) { + DockerRunConfig createDockerRunConfig(String network, Map environmentMap) { + if (network == null && environmentMap == null) { return null; } - List env = new ArrayList<>(); - boolean isNetworkDisabled = dockerFlagsDTO.network() != null && dockerFlagsDTO.network().equals("none"); + List environmentStrings = new ArrayList<>(); + boolean isNetworkDisabled = network != null && network.equals("none"); - if (dockerFlagsDTO.env() != null) { - for (Map.Entry entry : dockerFlagsDTO.env().entrySet()) { + if (environmentMap != null) { + for (Map.Entry entry : environmentMap.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); - env.add(key + "=" + value); + environmentStrings.add(key + "=" + value); } } - return new DockerRunConfig(isNetworkDisabled, env); + return new DockerRunConfig(isNetworkDisabled, environmentStrings); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseService.java index 886fa2622c8d..a5d448daa272 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseService.java @@ -1102,7 +1102,7 @@ public void validateDockerFlags(ProgrammingExercise programmingExercise) { } } - DockerRunConfig dockerRunConfig = programmingExerciseBuildConfigService.getDockerRunConfigFromParsedFlags(dockerFlagsDTO); + DockerRunConfig dockerRunConfig = programmingExerciseBuildConfigService.createDockerRunConfig(dockerFlagsDTO.network(), dockerFlagsDTO.env()); if (List.of(ProgrammingLanguage.SWIFT, ProgrammingLanguage.HASKELL).contains(programmingExercise.getProgrammingLanguage()) && dockerRunConfig.isNetworkDisabled()) { throw new BadRequestAlertException("This programming language does not support disabling the network access feature", "Exercise", "networkAccessNotSupported"); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingLanguageFeatureService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingLanguageFeatureService.java index e6049a4221d2..22f29fa2b041 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingLanguageFeatureService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingLanguageFeatureService.java @@ -1,14 +1,17 @@ package de.tum.cit.aet.artemis.programming.service; -import java.util.HashMap; +import java.util.EnumMap; import java.util.Map; +import jakarta.annotation.Nullable; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.actuate.info.Info; import org.springframework.boot.actuate.info.InfoContributor; import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage; +import de.tum.cit.aet.artemis.programming.domain.ProjectType; /** * This service provides information about features the different ProgrammingLanguages support. @@ -18,7 +21,16 @@ public abstract class ProgrammingLanguageFeatureService implements InfoContribut private static final Logger log = LoggerFactory.getLogger(ProgrammingLanguageFeatureService.class); - protected final Map programmingLanguageFeatures = new HashMap<>(); + private final LicenseService licenseService; + + private final Map programmingLanguageFeatures; + + protected ProgrammingLanguageFeatureService(LicenseService licenseService) { + this.licenseService = licenseService; + this.programmingLanguageFeatures = getEnabledFeatures(); + } + + protected abstract Map getSupportedProgrammingLanguageFeatures(); /** * Get the ProgrammingLanguageFeature configured for the given ProgrammingLanguage. @@ -37,12 +49,44 @@ public ProgrammingLanguageFeature getProgrammingLanguageFeatures(ProgrammingLang return programmingLanguageFeature; } - public Map getProgrammingLanguageFeatures() { - return programmingLanguageFeatures; - } - @Override public void contribute(Info.Builder builder) { - builder.withDetail("programmingLanguageFeatures", getProgrammingLanguageFeatures().values()); + builder.withDetail("programmingLanguageFeatures", programmingLanguageFeatures.values()); + } + + private Map getEnabledFeatures() { + var features = new EnumMap(ProgrammingLanguage.class); + for (var programmingLanguageFeatureEntry : getSupportedProgrammingLanguageFeatures().entrySet()) { + var language = programmingLanguageFeatureEntry.getKey(); + var feature = programmingLanguageFeatureEntry.getValue(); + if (feature.projectTypes().isEmpty()) { + if (isProjectTypeUsable(language, null)) { + features.put(language, feature); + } + } + else { + var filteredProjectTypes = feature.projectTypes().stream().filter((projectType -> isProjectTypeUsable(language, projectType))).toList(); + if (!filteredProjectTypes.isEmpty()) { + // @formatter:off + var filteredFeature = new ProgrammingLanguageFeature( + feature.programmingLanguage(), + feature.sequentialTestRuns(), + feature.staticCodeAnalysis(), + feature.plagiarismCheckSupported(), + feature.packageNameRequired(), + feature.checkoutSolutionRepositoryAllowed(), + filteredProjectTypes, + feature.auxiliaryRepositoriesSupported() + ); + // @formatter:on + features.put(language, filteredFeature); + } + } + } + return features; + } + + private boolean isProjectTypeUsable(ProgrammingLanguage programmingLanguage, @Nullable ProjectType projectType) { + return licenseService.isLicensed(programmingLanguage, projectType); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/TemplateUpgradePolicyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/TemplateUpgradePolicyService.java index 9dd3249bd1a4..d066241c7211 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/TemplateUpgradePolicyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/TemplateUpgradePolicyService.java @@ -32,9 +32,9 @@ public TemplateUpgradePolicyService(JavaTemplateUpgradeService javaRepositoryUpg public TemplateUpgradeService getUpgradeService(ProgrammingLanguage programmingLanguage) { return switch (programmingLanguage) { case JAVA -> javaRepositoryUpgradeService; - case KOTLIN, PYTHON, C, HASKELL, VHDL, ASSEMBLER, SWIFT, OCAML, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS, TYPESCRIPT, C_SHARP, GO, BASH -> + case KOTLIN, PYTHON, C, HASKELL, VHDL, ASSEMBLER, SWIFT, OCAML, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS, TYPESCRIPT, C_SHARP, GO, BASH, MATLAB -> defaultRepositoryUpgradeService; - case SQL, MATLAB, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + programmingLanguage); + case SQL, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + programmingLanguage); }; } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ci/ContinuousIntegrationService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ci/ContinuousIntegrationService.java index 3f14c1b2a015..c03b64a6022c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ci/ContinuousIntegrationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ci/ContinuousIntegrationService.java @@ -219,8 +219,9 @@ enum RepositoryCheckoutPath implements CustomizableCheckoutPath { @Override public String forProgrammingLanguage(ProgrammingLanguage language) { return switch (language) { - case JAVA, PYTHON, C, HASKELL, KOTLIN, VHDL, ASSEMBLER, SWIFT, OCAML, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS, TYPESCRIPT, C_SHARP, GO, BASH -> "assignment"; - case SQL, MATLAB, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + language); + case JAVA, PYTHON, C, HASKELL, KOTLIN, VHDL, ASSEMBLER, SWIFT, OCAML, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS, TYPESCRIPT, C_SHARP, GO, BASH, MATLAB -> + "assignment"; + case SQL, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + language); }; } }, @@ -230,8 +231,8 @@ public String forProgrammingLanguage(ProgrammingLanguage language) { public String forProgrammingLanguage(ProgrammingLanguage language) { return switch (language) { case JAVA, PYTHON, HASKELL, KOTLIN, SWIFT, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS, TYPESCRIPT -> ""; - case C, VHDL, ASSEMBLER, OCAML, C_SHARP, GO, BASH -> "tests"; - case SQL, MATLAB, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + language); + case C, VHDL, ASSEMBLER, OCAML, C_SHARP, GO, BASH, MATLAB -> "tests"; + case SQL, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + language); }; } }, diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/gitlabci/GitLabCIProgrammingLanguageFeatureService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/gitlabci/GitLabCIProgrammingLanguageFeatureService.java index 357cf05d97f8..7035d1235ccb 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/gitlabci/GitLabCIProgrammingLanguageFeatureService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/gitlabci/GitLabCIProgrammingLanguageFeatureService.java @@ -7,11 +7,15 @@ import static de.tum.cit.aet.artemis.programming.domain.ProjectType.MAVEN_MAVEN; import static de.tum.cit.aet.artemis.programming.domain.ProjectType.PLAIN_MAVEN; +import java.util.EnumMap; import java.util.List; +import java.util.Map; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; +import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage; +import de.tum.cit.aet.artemis.programming.service.LicenseService; import de.tum.cit.aet.artemis.programming.service.ProgrammingLanguageFeature; import de.tum.cit.aet.artemis.programming.service.ProgrammingLanguageFeatureService; @@ -22,10 +26,17 @@ @Profile("gitlabci") public class GitLabCIProgrammingLanguageFeatureService extends ProgrammingLanguageFeatureService { - public GitLabCIProgrammingLanguageFeatureService() { + protected GitLabCIProgrammingLanguageFeatureService(LicenseService licenseService) { + super(licenseService); + } + + @Override + protected Map getSupportedProgrammingLanguageFeatures() { + EnumMap programmingLanguageFeatures = new EnumMap<>(ProgrammingLanguage.class); programmingLanguageFeatures.put(EMPTY, new ProgrammingLanguageFeature(EMPTY, false, false, false, false, false, List.of(), false)); programmingLanguageFeatures.put(JAVA, new ProgrammingLanguageFeature(JAVA, false, false, false, true, false, List.of(PLAIN_MAVEN, MAVEN_MAVEN), false)); programmingLanguageFeatures.put(JAVASCRIPT, new ProgrammingLanguageFeature(JAVASCRIPT, false, false, true, false, false, List.of(), false)); programmingLanguageFeatures.put(RUST, new ProgrammingLanguageFeature(RUST, false, false, true, false, false, List.of(), false)); + return programmingLanguageFeatures; } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/JenkinsProgrammingLanguageFeatureService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/JenkinsProgrammingLanguageFeatureService.java index ec4a7dd3598a..eb8a47580c43 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/JenkinsProgrammingLanguageFeatureService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/JenkinsProgrammingLanguageFeatureService.java @@ -25,11 +25,15 @@ import static de.tum.cit.aet.artemis.programming.domain.ProjectType.PLAIN_GRADLE; import static de.tum.cit.aet.artemis.programming.domain.ProjectType.PLAIN_MAVEN; +import java.util.EnumMap; import java.util.List; +import java.util.Map; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; +import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage; +import de.tum.cit.aet.artemis.programming.service.LicenseService; import de.tum.cit.aet.artemis.programming.service.ProgrammingLanguageFeature; import de.tum.cit.aet.artemis.programming.service.ProgrammingLanguageFeatureService; @@ -37,8 +41,14 @@ @Profile(PROFILE_JENKINS) public class JenkinsProgrammingLanguageFeatureService extends ProgrammingLanguageFeatureService { - public JenkinsProgrammingLanguageFeatureService() { + protected JenkinsProgrammingLanguageFeatureService(LicenseService licenseService) { + super(licenseService); + } + + @Override + protected Map getSupportedProgrammingLanguageFeatures() { // Must be extended once a new programming language is added + EnumMap programmingLanguageFeatures = new EnumMap<>(ProgrammingLanguage.class); programmingLanguageFeatures.put(EMPTY, new ProgrammingLanguageFeature(EMPTY, false, false, false, false, false, List.of(), false)); programmingLanguageFeatures.put(BASH, new ProgrammingLanguageFeature(BASH, false, false, false, false, false, List.of(), false)); programmingLanguageFeatures.put(C, new ProgrammingLanguageFeature(C, false, false, true, false, false, List.of(FACT, GCC), false)); @@ -56,5 +66,6 @@ public JenkinsProgrammingLanguageFeatureService() { // Jenkins is not supporting XCODE at the moment programmingLanguageFeatures.put(SWIFT, new ProgrammingLanguageFeature(SWIFT, false, true, true, true, false, List.of(PLAIN), false)); programmingLanguageFeatures.put(TYPESCRIPT, new ProgrammingLanguageFeature(TYPESCRIPT, false, false, true, false, false, List.of(), false)); + return programmingLanguageFeatures; } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIProgrammingLanguageFeatureService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIProgrammingLanguageFeatureService.java index e9c7e13b8659..41cdabd90bda 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIProgrammingLanguageFeatureService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIProgrammingLanguageFeatureService.java @@ -12,6 +12,7 @@ import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.JAVA; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.JAVASCRIPT; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.KOTLIN; +import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.MATLAB; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.OCAML; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.PYTHON; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.R; @@ -27,11 +28,15 @@ import static de.tum.cit.aet.artemis.programming.domain.ProjectType.PLAIN_GRADLE; import static de.tum.cit.aet.artemis.programming.domain.ProjectType.PLAIN_MAVEN; +import java.util.EnumMap; import java.util.List; +import java.util.Map; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; +import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage; +import de.tum.cit.aet.artemis.programming.service.LicenseService; import de.tum.cit.aet.artemis.programming.service.ProgrammingLanguageFeature; import de.tum.cit.aet.artemis.programming.service.ProgrammingLanguageFeatureService; @@ -42,8 +47,14 @@ @Profile(PROFILE_LOCALCI) public class LocalCIProgrammingLanguageFeatureService extends ProgrammingLanguageFeatureService { - public LocalCIProgrammingLanguageFeatureService() { + protected LocalCIProgrammingLanguageFeatureService(LicenseService licenseService) { + super(licenseService); + } + + @Override + protected Map getSupportedProgrammingLanguageFeatures() { // Must be extended once a new programming language is added + EnumMap programmingLanguageFeatures = new EnumMap<>(ProgrammingLanguage.class); programmingLanguageFeatures.put(EMPTY, new ProgrammingLanguageFeature(EMPTY, false, false, false, false, false, List.of(), true)); programmingLanguageFeatures.put(ASSEMBLER, new ProgrammingLanguageFeature(ASSEMBLER, false, false, false, false, false, List.of(), true)); programmingLanguageFeatures.put(BASH, new ProgrammingLanguageFeature(BASH, false, false, false, false, false, List.of(), true)); @@ -56,6 +67,7 @@ public LocalCIProgrammingLanguageFeatureService() { new ProgrammingLanguageFeature(JAVA, true, true, true, true, false, List.of(PLAIN_GRADLE, GRADLE_GRADLE, PLAIN_MAVEN, MAVEN_MAVEN), true)); programmingLanguageFeatures.put(JAVASCRIPT, new ProgrammingLanguageFeature(JAVASCRIPT, false, false, true, false, false, List.of(), true)); programmingLanguageFeatures.put(KOTLIN, new ProgrammingLanguageFeature(KOTLIN, false, false, true, true, false, List.of(), true)); + programmingLanguageFeatures.put(MATLAB, new ProgrammingLanguageFeature(MATLAB, false, false, false, false, false, List.of(), true)); programmingLanguageFeatures.put(OCAML, new ProgrammingLanguageFeature(OCAML, false, false, false, false, true, List.of(), true)); programmingLanguageFeatures.put(PYTHON, new ProgrammingLanguageFeature(PYTHON, false, true, true, false, false, List.of(), true)); programmingLanguageFeatures.put(R, new ProgrammingLanguageFeature(R, false, false, true, false, false, List.of(), true)); @@ -63,5 +75,6 @@ public LocalCIProgrammingLanguageFeatureService() { programmingLanguageFeatures.put(SWIFT, new ProgrammingLanguageFeature(SWIFT, false, false, true, true, false, List.of(PLAIN), true)); programmingLanguageFeatures.put(TYPESCRIPT, new ProgrammingLanguageFeature(TYPESCRIPT, false, false, true, false, false, List.of(), true)); programmingLanguageFeatures.put(VHDL, new ProgrammingLanguageFeature(VHDL, false, false, false, false, false, List.of(), true)); + return programmingLanguageFeatures; } } diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml index e9aebc1980b1..5e4b7713fe50 100644 --- a/src/main/resources/config/application.yml +++ b/src/main/resources/config/application.yml @@ -105,6 +105,8 @@ artemis: default: "ghcr.io/ls1intum/artemis-javascript-docker:v1.0.0" go: default: "ghcr.io/ls1intum/artemis-go-docker:v1.0.0" + matlab: + default: "mathworks/matlab:r2024b" # The following properties are used to configure the Artemis build agent. # The build agent is responsible for executing the buildJob to test student submissions. diff --git a/src/main/resources/templates/aeolus/matlab/default.sh b/src/main/resources/templates/aeolus/matlab/default.sh new file mode 100644 index 000000000000..1c44cf9a481a --- /dev/null +++ b/src/main/resources/templates/aeolus/matlab/default.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -e + +test () { + echo '⚙️ executing test' + cd "${testWorkingDirectory}" + + sudo mkdir test-results + sudo chown matlab:matlab test-results + sudo rm /etc/sudoers.d/matlab + + matlab -batch testRunner + +} + +main () { + test +} + +main "${@}" diff --git a/src/main/resources/templates/aeolus/matlab/default.yaml b/src/main/resources/templates/aeolus/matlab/default.yaml new file mode 100644 index 000000000000..e2a61f4b62f2 --- /dev/null +++ b/src/main/resources/templates/aeolus/matlab/default.yaml @@ -0,0 +1,18 @@ +api: v0.0.1 +metadata: + name: "MATLAB" + id: matlab +actions: + - name: test + script: | + cd "${testWorkingDirectory}" + + sudo mkdir test-results + sudo chown matlab:matlab test-results + sudo rm /etc/sudoers.d/matlab + + matlab -batch testRunner + results: + - name: Test Results + path: "${testWorkingDirectory}/test-results/results.xml" + type: junit diff --git a/src/main/resources/templates/matlab/exercise/.gitattributes b/src/main/resources/templates/matlab/exercise/.gitattributes new file mode 100644 index 000000000000..7871ff363b73 --- /dev/null +++ b/src/main/resources/templates/matlab/exercise/.gitattributes @@ -0,0 +1,28 @@ +* text=auto + +*.fig binary +*.mat binary +*.mdl binary diff merge=mlAutoMerge +*.mdlp binary +*.mex* binary +*.mlapp binary +*.mldatx binary +*.mlproj binary +*.mlx binary +*.p binary +*.sfx binary +*.sldd binary +*.slreqx binary merge=mlAutoMerge +*.slmx binary merge=mlAutoMerge +*.sltx binary +*.slxc binary +*.slx binary merge=mlAutoMerge +*.slxp binary + +## Other common binary file types +*.docx binary +*.exe binary +*.jpg binary +*.pdf binary +*.png binary +*.xlsx binary diff --git a/src/main/resources/templates/matlab/exercise/.gitignore b/src/main/resources/templates/matlab/exercise/.gitignore new file mode 100644 index 000000000000..ce3b7fbfc90e --- /dev/null +++ b/src/main/resources/templates/matlab/exercise/.gitignore @@ -0,0 +1,36 @@ +# Autosave files +*.asv +*.m~ +*.autosave +*.slx.r* +*.mdl.r* + +# Derived content-obscured files +*.p + +# Compiled MEX files +*.mex* + +# Packaged app and toolbox files +*.mlappinstall +*.mltbx + +# Deployable archives +*.ctf + +# Generated helpsearch folders +helpsearch*/ + +# Code generation folders +slprj/ +sccprj/ +codegen/ + +# Cache files +*.slxc + +# Cloud based storage dotfile +.MATLABDriveTag + +# buildtool cache folder +.buildtool/ diff --git a/src/main/resources/templates/matlab/exercise/averageGradeByStudent.m b/src/main/resources/templates/matlab/exercise/averageGradeByStudent.m new file mode 100644 index 000000000000..847a4cbb6fd9 --- /dev/null +++ b/src/main/resources/templates/matlab/exercise/averageGradeByStudent.m @@ -0,0 +1,3 @@ +function avg = averageGradeByStudent(grades) + % TODO: Task 2 +end diff --git a/src/main/resources/templates/matlab/exercise/finalGrade.m b/src/main/resources/templates/matlab/exercise/finalGrade.m new file mode 100644 index 000000000000..863131c288d8 --- /dev/null +++ b/src/main/resources/templates/matlab/exercise/finalGrade.m @@ -0,0 +1,3 @@ +function g = finalGrade(grades,weights) + % TODO: Task 3 +end diff --git a/src/main/resources/templates/matlab/exercise/medianGradeByAssignment.m b/src/main/resources/templates/matlab/exercise/medianGradeByAssignment.m new file mode 100644 index 000000000000..7ebeb2e39b3a --- /dev/null +++ b/src/main/resources/templates/matlab/exercise/medianGradeByAssignment.m @@ -0,0 +1,3 @@ +function avg = medianGradeByAssignment(grades) + % TODO: Task 1 +end diff --git a/src/main/resources/templates/matlab/readme b/src/main/resources/templates/matlab/readme new file mode 100644 index 000000000000..0a5ee1f0f265 --- /dev/null +++ b/src/main/resources/templates/matlab/readme @@ -0,0 +1,49 @@ +# Grading Statistics + +In this exercise, you will analyze student performance based on a matrix of +grades. Each row in the matrix represents a student, and each column represents +an assignment. + +Your task is to implement three functions that compute statistical measures for +this data. Each function has a specific purpose, described below: + +## Task 1: Median Grade by Assignment +Compute the median grade for each assignment across all students. The result +should be a row vector where each element corresponds to the median of a +specific assignment (column). + +- **Function to Implement**: `medianGradeByAssignment()` +- **Input**: A matrix `grades` where rows are students and columns are assignments. +- **Output**: A row vector of medians, one for each assignment. + +[task][Median Grade by Assignment](testMedianGradeByAssignment) + +--- + +## Task 2: Average Grade by Student +Calculate the arithmetic mean grade for each student across all their assignments. +The result should be a row vector where each element corresponds to a student's +arithmetic mean grade. + +- **Function to Implement**: `averageGradeByStudent()` +- **Input**: A matrix `grades` where rows are students and columns are assignments. +- **Output**: A row vector of averages, one for each student. + +[task][Average Grade by Student](testAverageGradeByStudent) + +--- + +## Task 3: Final Grade +Determine the weighted final grade for each student. In addition to the +`grades` matrix, you are provided with a row vector `weights` (summing to 1) +that represents the weight of each assignment. Compute the weighted average for +each student and round the result to 1 decimal place. The result should be a +row vector where each element corresponds to a student’s final grade. + +- **Function to Implement**: `finalGrade()` +- **Input**: + 1. A matrix `grades` where rows are students and columns are assignments. + 2. A row vector `weights` with the same number of elements as columns in the `grades` matrix. +- **Output**: A row vector of weighted final grades, rounded to 1 decimal place. + +[task][Final Grade](testFinalGrade) diff --git a/src/main/resources/templates/matlab/solution/.gitattributes b/src/main/resources/templates/matlab/solution/.gitattributes new file mode 100644 index 000000000000..7871ff363b73 --- /dev/null +++ b/src/main/resources/templates/matlab/solution/.gitattributes @@ -0,0 +1,28 @@ +* text=auto + +*.fig binary +*.mat binary +*.mdl binary diff merge=mlAutoMerge +*.mdlp binary +*.mex* binary +*.mlapp binary +*.mldatx binary +*.mlproj binary +*.mlx binary +*.p binary +*.sfx binary +*.sldd binary +*.slreqx binary merge=mlAutoMerge +*.slmx binary merge=mlAutoMerge +*.sltx binary +*.slxc binary +*.slx binary merge=mlAutoMerge +*.slxp binary + +## Other common binary file types +*.docx binary +*.exe binary +*.jpg binary +*.pdf binary +*.png binary +*.xlsx binary diff --git a/src/main/resources/templates/matlab/solution/.gitignore b/src/main/resources/templates/matlab/solution/.gitignore new file mode 100644 index 000000000000..ce3b7fbfc90e --- /dev/null +++ b/src/main/resources/templates/matlab/solution/.gitignore @@ -0,0 +1,36 @@ +# Autosave files +*.asv +*.m~ +*.autosave +*.slx.r* +*.mdl.r* + +# Derived content-obscured files +*.p + +# Compiled MEX files +*.mex* + +# Packaged app and toolbox files +*.mlappinstall +*.mltbx + +# Deployable archives +*.ctf + +# Generated helpsearch folders +helpsearch*/ + +# Code generation folders +slprj/ +sccprj/ +codegen/ + +# Cache files +*.slxc + +# Cloud based storage dotfile +.MATLABDriveTag + +# buildtool cache folder +.buildtool/ diff --git a/src/main/resources/templates/matlab/solution/averageGradeByStudent.m b/src/main/resources/templates/matlab/solution/averageGradeByStudent.m new file mode 100644 index 000000000000..6bc13004967a --- /dev/null +++ b/src/main/resources/templates/matlab/solution/averageGradeByStudent.m @@ -0,0 +1,3 @@ +function avg = averageGradeByStudent(grades) + avg = mean(grades,2).'; +end diff --git a/src/main/resources/templates/matlab/solution/finalGrade.m b/src/main/resources/templates/matlab/solution/finalGrade.m new file mode 100644 index 000000000000..4fdfe1c2b8bb --- /dev/null +++ b/src/main/resources/templates/matlab/solution/finalGrade.m @@ -0,0 +1,3 @@ +function g = finalGrade(grades,weights) + g = round((grades * weights.').',1); +end diff --git a/src/main/resources/templates/matlab/solution/medianGradeByAssignment.m b/src/main/resources/templates/matlab/solution/medianGradeByAssignment.m new file mode 100644 index 000000000000..31d5fe2b5800 --- /dev/null +++ b/src/main/resources/templates/matlab/solution/medianGradeByAssignment.m @@ -0,0 +1,3 @@ +function avg = medianGradeByAssignment(grades) + avg = median(grades); +end diff --git a/src/main/resources/templates/matlab/test/.gitattributes b/src/main/resources/templates/matlab/test/.gitattributes new file mode 100644 index 000000000000..7871ff363b73 --- /dev/null +++ b/src/main/resources/templates/matlab/test/.gitattributes @@ -0,0 +1,28 @@ +* text=auto + +*.fig binary +*.mat binary +*.mdl binary diff merge=mlAutoMerge +*.mdlp binary +*.mex* binary +*.mlapp binary +*.mldatx binary +*.mlproj binary +*.mlx binary +*.p binary +*.sfx binary +*.sldd binary +*.slreqx binary merge=mlAutoMerge +*.slmx binary merge=mlAutoMerge +*.sltx binary +*.slxc binary +*.slx binary merge=mlAutoMerge +*.slxp binary + +## Other common binary file types +*.docx binary +*.exe binary +*.jpg binary +*.pdf binary +*.png binary +*.xlsx binary diff --git a/src/main/resources/templates/matlab/test/.gitignore b/src/main/resources/templates/matlab/test/.gitignore new file mode 100644 index 000000000000..24cee4ed66ae --- /dev/null +++ b/src/main/resources/templates/matlab/test/.gitignore @@ -0,0 +1,39 @@ +# Test results +results.xml + +# Autosave files +*.asv +*.m~ +*.autosave +*.slx.r* +*.mdl.r* + +# Derived content-obscured files +*.p + +# Compiled MEX files +*.mex* + +# Packaged app and toolbox files +*.mlappinstall +*.mltbx + +# Deployable archives +*.ctf + +# Generated helpsearch folders +helpsearch*/ + +# Code generation folders +slprj/ +sccprj/ +codegen/ + +# Cache files +*.slxc + +# Cloud based storage dotfile +.MATLABDriveTag + +# buildtool cache folder +.buildtool/ diff --git a/src/main/resources/templates/matlab/test/testRunner.m b/src/main/resources/templates/matlab/test/testRunner.m new file mode 100644 index 000000000000..1e409e2de0dd --- /dev/null +++ b/src/main/resources/templates/matlab/test/testRunner.m @@ -0,0 +1,12 @@ +import matlab.unittest.plugins.XMLPlugin + +addpath("../${studentParentWorkingDirectoryName}") + +runner = testrunner; + +plugin = XMLPlugin.producingJUnitFormat("test-results/results.xml"); +addPlugin(runner,plugin); + +suite = testsuite("tests"); + +run(runner,suite); diff --git a/src/main/resources/templates/matlab/test/tests/GradeTest.m b/src/main/resources/templates/matlab/test/tests/GradeTest.m new file mode 100644 index 000000000000..3511c887274f --- /dev/null +++ b/src/main/resources/templates/matlab/test/tests/GradeTest.m @@ -0,0 +1,45 @@ +classdef GradeTest < matlab.unittest.TestCase + properties + grades + end + + methods (TestClassSetup) + % Shared setup for the entire test class + end + + methods (TestMethodSetup) + % Setup for each test + + function setupGrades(testCase) + testCase.grades = [1.3 3.3 4.0 4.7 + 2.7 1.7 4.0 1.7 + 1.7 3.7 3.0 4.3 + 4.3 2.3 1.7 3.3 + 2.3 3.7 2.0 5.0]; + end + end + + methods (Test) + % Test methods + + function testMedianGradeByAssignment(testCase) + actual = medianGradeByAssignment(testCase.grades); + expected = [2.3 3.3 3.0 4.3]; + testCase.assertEqual(actual,expected,"median is incorrect",AbsTol=0.0001); + end + + function testAverageGradeByStudent(testCase) + actual = averageGradeByStudent(testCase.grades); + expected = [3.325 2.525 3.175 2.9 3.25]; + testCase.assertEqual(actual,expected,"average is incorrect",AbsTol=0.0001); + end + + function testFinalGrade(testCase) + weights = [0.1 0.1 0.5 0.3]; + actual = finalGrade(testCase.grades,weights); + expected = [3.9 3.0 3.3 2.5 3.1]; + testCase.assertEqual(actual,expected,"final grades are incorrect",AbsTol=0.0001); + end + end + +end diff --git a/src/main/webapp/app/entities/programming/programming-exercise.model.ts b/src/main/webapp/app/entities/programming/programming-exercise.model.ts index ece9e6d6989c..8b5e1b0136b3 100644 --- a/src/main/webapp/app/entities/programming/programming-exercise.model.ts +++ b/src/main/webapp/app/entities/programming/programming-exercise.model.ts @@ -23,6 +23,7 @@ export enum ProgrammingLanguage { JAVA = 'JAVA', JAVASCRIPT = 'JAVASCRIPT', KOTLIN = 'KOTLIN', + MATLAB = 'MATLAB', OCAML = 'OCAML', PYTHON = 'PYTHON', R = 'R', diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts b/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts index c3eff649807f..0cbb2cffaff5 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts @@ -230,7 +230,7 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { ); } this.supportsAuxiliaryRepositories = - this.programmingLanguageFeatureService.getProgrammingLanguageFeature(programmingExercise.programmingLanguage).auxiliaryRepositoriesSupported ?? + this.programmingLanguageFeatureService.getProgrammingLanguageFeature(programmingExercise.programmingLanguage)?.auxiliaryRepositoriesSupported ?? false; this.localVCEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALVC); this.localCIEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALCI); @@ -269,9 +269,8 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { .subscribe({ next: () => { this.checkAndAlertInconsistencies(); - this.plagiarismCheckSupported = this.programmingLanguageFeatureService.getProgrammingLanguageFeature( - programmingExercise.programmingLanguage, - ).plagiarismCheckSupported; + this.plagiarismCheckSupported = + this.programmingLanguageFeatureService.getProgrammingLanguageFeature(programmingExercise.programmingLanguage)?.plagiarismCheckSupported ?? false; /** we make sure to await the results of the subscriptions (switchMap) to only call {@link getExerciseDetails} once */ this.exerciseDetailSections = this.getExerciseDetails(); diff --git a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.ts b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.ts index 732a969f49b0..6dfa8ae91c74 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.ts @@ -289,7 +289,7 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest const languageChanged = this.selectedProgrammingLanguageValue !== language; this.selectedProgrammingLanguageValue = language; - const programmingLanguageFeature = this.programmingLanguageFeatureService.getProgrammingLanguageFeature(language); + const programmingLanguageFeature = this.programmingLanguageFeatureService.getProgrammingLanguageFeature(language)!; this.packageNameRequired = programmingLanguageFeature?.packageNameRequired; this.staticCodeAnalysisAllowed = programmingLanguageFeature.staticCodeAnalysis; this.checkoutSolutionRepositoryAllowed = programmingLanguageFeature.checkoutSolutionRepositoryAllowed; @@ -377,7 +377,7 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest // update the project types for java programming exercises according to whether dependencies should be included if (this.programmingExercise.programmingLanguage === ProgrammingLanguage.JAVA) { - const programmingLanguageFeature = this.programmingLanguageFeatureService.getProgrammingLanguageFeature(ProgrammingLanguage.JAVA); + const programmingLanguageFeature = this.programmingLanguageFeatureService.getProgrammingLanguageFeature(ProgrammingLanguage.JAVA)!; if (type == ProjectType.MAVEN_BLACKBOX) { this.selectedProjectTypeValue = ProjectType.MAVEN_BLACKBOX; this.programmingExercise.projectType = ProjectType.MAVEN_BLACKBOX; diff --git a/src/main/webapp/app/exercises/programming/shared/service/programming-language-feature/programming-language-feature.service.ts b/src/main/webapp/app/exercises/programming/shared/service/programming-language-feature/programming-language-feature.service.ts index 9188c2afaea0..8fca224ebfa7 100644 --- a/src/main/webapp/app/exercises/programming/shared/service/programming-language-feature/programming-language-feature.service.ts +++ b/src/main/webapp/app/exercises/programming/shared/service/programming-language-feature/programming-language-feature.service.ts @@ -31,8 +31,8 @@ export class ProgrammingLanguageFeatureService { }); } - public getProgrammingLanguageFeature(programmingLanguage: ProgrammingLanguage): ProgrammingLanguageFeature { - return this.programmingLanguageFeatures.get(programmingLanguage)!; + public getProgrammingLanguageFeature(programmingLanguage: ProgrammingLanguage): ProgrammingLanguageFeature | undefined { + return this.programmingLanguageFeatures.get(programmingLanguage); } public supportsProgrammingLanguage(programmingLanguage: ProgrammingLanguage): boolean { diff --git a/src/test/java/de/tum/cit/aet/artemis/core/config/LicenseConfigurationTest.java b/src/test/java/de/tum/cit/aet/artemis/core/config/LicenseConfigurationTest.java new file mode 100644 index 000000000000..d6ea2691f4d2 --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/core/config/LicenseConfigurationTest.java @@ -0,0 +1,27 @@ +package de.tum.cit.aet.artemis.core.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class LicenseConfigurationTest { + + @Test + void testMatlabLicenseServer() { + String licenseServer = "1234@license-server"; + LicenseConfiguration licenseConfiguration = new LicenseConfiguration(new LicenseConfiguration.MatLabLicense(licenseServer)); + assertThat(licenseConfiguration.getMatlabLicenseServer()).isEqualTo(licenseServer); + } + + @Test + void testMatlabNullRecord() { + LicenseConfiguration licenseConfiguration = new LicenseConfiguration(null); + assertThat(licenseConfiguration.getMatlabLicenseServer()).isNull(); + } + + @Test + void testMatlabNullValue() { + LicenseConfiguration licenseConfiguration = new LicenseConfiguration(new LicenseConfiguration.MatLabLicense(null)); + assertThat(licenseConfiguration.getMatlabLicenseServer()).isNull(); + } +} diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/service/LicenseServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/service/LicenseServiceTest.java new file mode 100644 index 000000000000..86621e4042d8 --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/programming/service/LicenseServiceTest.java @@ -0,0 +1,51 @@ +package de.tum.cit.aet.artemis.programming.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import de.tum.cit.aet.artemis.core.config.LicenseConfiguration; +import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage; +import de.tum.cit.aet.artemis.programming.domain.ProjectType; +import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; + +class LicenseServiceTest extends AbstractSpringIntegrationIndependentTest { + + @Autowired + private LicenseService licenseService; + + @Test + void testIsLicensedNoneRequired() { + boolean isLicensed = licenseService.isLicensed(ProgrammingLanguage.JAVA, ProjectType.GRADLE_GRADLE); + assertThat(isLicensed).isTrue(); + } + + @Test + void testGetLicenseNoneRequired() { + Map environment = licenseService.getEnvironment(ProgrammingLanguage.JAVA, ProjectType.GRADLE_GRADLE); + assertThat(environment).isEmpty(); + } + + @Test + void testIsLicensedMatlab() { + boolean isLicensed = licenseService.isLicensed(ProgrammingLanguage.MATLAB, null); + assertThat(isLicensed).isTrue(); + } + + @Test + void testGetLicenseMatlab() { + Map environment = licenseService.getEnvironment(ProgrammingLanguage.MATLAB, null); + assertThat(environment).containsEntry("MLM_LICENSE_FILE", "1234@license-server"); + } + + @Test + void testIsLicensedMatlabUnlicensed() { + LicenseConfiguration licenseConfiguration = new LicenseConfiguration(null); + LicenseService licenseService = new LicenseService(licenseConfiguration); + boolean isLicensed = licenseService.isLicensed(ProgrammingLanguage.MATLAB, null); + assertThat(isLicensed).isFalse(); + } +} diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/util/ArgumentSources.java b/src/test/java/de/tum/cit/aet/artemis/programming/util/ArgumentSources.java index c8b66beb167f..f7c94236f82b 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/util/ArgumentSources.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/util/ArgumentSources.java @@ -1,6 +1,7 @@ package de.tum.cit.aet.artemis.programming.util; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.ASSEMBLER; +import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.MATLAB; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.OCAML; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.VHDL; @@ -14,7 +15,7 @@ public class ArgumentSources { // TODO: Add template for VHDL, Assembler, and Ocaml and activate those languages here again public static Set generateJenkinsSupportedLanguages() { - List unsupportedLanguages = List.of(VHDL, ASSEMBLER, OCAML); + List unsupportedLanguages = List.of(VHDL, ASSEMBLER, OCAML, MATLAB); var supportedLanguages = EnumSet.copyOf(ProgrammingLanguage.getEnabledLanguages()); unsupportedLanguages.forEach(supportedLanguages::remove); diff --git a/src/test/java/de/tum/cit/aet/artemis/shared/base/AbstractSpringIntegrationLocalCILocalVCTest.java b/src/test/java/de/tum/cit/aet/artemis/shared/base/AbstractSpringIntegrationLocalCILocalVCTest.java index 2538b01761f7..83333e3e6049 100644 --- a/src/test/java/de/tum/cit/aet/artemis/shared/base/AbstractSpringIntegrationLocalCILocalVCTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/shared/base/AbstractSpringIntegrationLocalCILocalVCTest.java @@ -245,7 +245,7 @@ protected static void mockDockerClient() throws InterruptedException { String dummyContainerId = "1234567890"; - // Mock dockerClient.createContainerCmd(String dockerImage).withHostConfig(HostConfig hostConfig).withEnv(String... env).withCmd(String... cmd).exec() + // Mock dockerClient.createContainerCmd(String dockerImage).withHostConfig(HostConfig hostConfig).withEnv(String... env).withEntrypoint().withCmd(String... cmd).exec() CreateContainerCmd createContainerCmd = mock(CreateContainerCmd.class); CreateContainerResponse createContainerResponse = new CreateContainerResponse(); createContainerResponse.setId(dummyContainerId); @@ -254,6 +254,7 @@ protected static void mockDockerClient() throws InterruptedException { doReturn(createContainerCmd).when(createContainerCmd).withHostConfig(any()); doReturn(createContainerCmd).when(createContainerCmd).withEnv(anyList()); doReturn(createContainerCmd).when(createContainerCmd).withUser(anyString()); + doReturn(createContainerCmd).when(createContainerCmd).withEntrypoint(); doReturn(createContainerCmd).when(createContainerCmd).withCmd(anyString(), anyString(), anyString()); doReturn(createContainerResponse).when(createContainerCmd).exec(); diff --git a/src/test/resources/config/application.yml b/src/test/resources/config/application.yml index 869514c89759..2eca321307c2 100644 --- a/src/test/resources/config/application.yml +++ b/src/test/resources/config/application.yml @@ -82,11 +82,16 @@ artemis: default: "~~invalid~~" go: default: "~~invalid~~" + matlab: + default: "~~invalid~~" build-agent: short-name: "artemis-build-agent-test" telemetry: enabled: false # setting this to false will disable sending any information to the telemetry service - + licenses: + matlab: + license-server: "1234@license-server" + spring: application: name: Artemis From 706679340f531fdb04f97337cfae370299e83d43 Mon Sep 17 00:00:00 2001 From: Marcel Gaupp Date: Thu, 30 Jan 2025 21:46:46 +0100 Subject: [PATCH 6/8] Programming exercises: Add Ruby programming exercise template (#10202) --- .../programming-exercise-features.inc | 4 + .../domain/ProgrammingLanguage.java | 1 + .../service/TemplateUpgradePolicyService.java | 4 +- .../ci/ContinuousIntegrationService.java | 8 +- ...kinsProgrammingLanguageFeatureService.java | 2 + .../build_plan/JenkinsBuildPlanService.java | 4 +- ...alCIProgrammingLanguageFeatureService.java | 2 + src/main/resources/config/application.yml | 2 + .../templates/aeolus/ruby/default.sh | 14 +++ .../templates/aeolus/ruby/default.yaml | 13 +++ .../jenkins/ruby/regularRuns/pipeline.groovy | 55 ++++++++++++ .../templates/ruby/exercise/.gitignore | 8 ++ .../resources/templates/ruby/exercise/Gemfile | 5 ++ .../templates/ruby/exercise/Gemfile.lock | 14 +++ .../ruby/exercise/src/bubble_sort.rb | 11 +++ .../templates/ruby/exercise/src/client.rb | 66 ++++++++++++++ .../templates/ruby/exercise/src/merge_sort.rb | 11 +++ src/main/resources/templates/ruby/readme | 89 +++++++++++++++++++ .../templates/ruby/solution/.gitignore | 8 ++ .../resources/templates/ruby/solution/Gemfile | 5 ++ .../templates/ruby/solution/Gemfile.lock | 14 +++ .../ruby/solution/src/bubble_sort.rb | 13 +++ .../templates/ruby/solution/src/client.rb | 64 +++++++++++++ .../templates/ruby/solution/src/context.rb | 13 +++ .../templates/ruby/solution/src/merge_sort.rb | 58 ++++++++++++ .../templates/ruby/solution/src/policy.rb | 24 +++++ .../resources/templates/ruby/test/.gitignore | 11 +++ .../resources/templates/ruby/test/Gemfile | 10 +++ .../templates/ruby/test/Gemfile.lock | 25 ++++++ .../resources/templates/ruby/test/Rakefile | 16 ++++ .../templates/ruby/test/assignment_path.rb | 3 + .../templates/ruby/test/test/test_behavior.rb | 64 +++++++++++++ .../templates/ruby/test/test/test_helper.rb | 12 +++ .../ruby/test/test/test_structural.rb | 31 +++++++ .../programming/programming-exercise.model.ts | 1 + src/test/resources/config/application.yml | 2 + 36 files changed, 679 insertions(+), 8 deletions(-) create mode 100644 src/main/resources/templates/aeolus/ruby/default.sh create mode 100644 src/main/resources/templates/aeolus/ruby/default.yaml create mode 100644 src/main/resources/templates/jenkins/ruby/regularRuns/pipeline.groovy create mode 100644 src/main/resources/templates/ruby/exercise/.gitignore create mode 100644 src/main/resources/templates/ruby/exercise/Gemfile create mode 100644 src/main/resources/templates/ruby/exercise/Gemfile.lock create mode 100644 src/main/resources/templates/ruby/exercise/src/bubble_sort.rb create mode 100755 src/main/resources/templates/ruby/exercise/src/client.rb create mode 100644 src/main/resources/templates/ruby/exercise/src/merge_sort.rb create mode 100644 src/main/resources/templates/ruby/readme create mode 100644 src/main/resources/templates/ruby/solution/.gitignore create mode 100644 src/main/resources/templates/ruby/solution/Gemfile create mode 100644 src/main/resources/templates/ruby/solution/Gemfile.lock create mode 100644 src/main/resources/templates/ruby/solution/src/bubble_sort.rb create mode 100755 src/main/resources/templates/ruby/solution/src/client.rb create mode 100644 src/main/resources/templates/ruby/solution/src/context.rb create mode 100644 src/main/resources/templates/ruby/solution/src/merge_sort.rb create mode 100644 src/main/resources/templates/ruby/solution/src/policy.rb create mode 100644 src/main/resources/templates/ruby/test/.gitignore create mode 100644 src/main/resources/templates/ruby/test/Gemfile create mode 100644 src/main/resources/templates/ruby/test/Gemfile.lock create mode 100644 src/main/resources/templates/ruby/test/Rakefile create mode 100644 src/main/resources/templates/ruby/test/assignment_path.rb create mode 100644 src/main/resources/templates/ruby/test/test/test_behavior.rb create mode 100644 src/main/resources/templates/ruby/test/test/test_helper.rb create mode 100644 src/main/resources/templates/ruby/test/test/test_structural.rb diff --git a/docs/user/exercises/programming-exercise-features.inc b/docs/user/exercises/programming-exercise-features.inc index 4c3fea5bf56e..8ba8ed672b07 100644 --- a/docs/user/exercises/programming-exercise-features.inc +++ b/docs/user/exercises/programming-exercise-features.inc @@ -51,6 +51,8 @@ Instructors can still use those templates to generate programming exercises and +----------------------+----------+---------+ | MATLAB | yes | no | +----------------------+----------+---------+ + | Ruby | yes | yes | + +----------------------+----------+---------+ - Not all ``templates`` support the same feature set and supported features can also change depending on the continuous integration system setup. Depending on the feature set, some options might not be available during the creation of the programming exercise. @@ -99,6 +101,8 @@ Instructors can still use those templates to generate programming exercises and +----------------------+----------------------+----------------------+---------------------+--------------+------------------------------------------+------------------------------+------------------------+ | MATLAB | no | no | no | no | n/a | no | L: yes, J: no | +----------------------+----------------------+----------------------+---------------------+--------------+------------------------------------------+------------------------------+------------------------+ + | Ruby | no | no | no | no | n/a | no | L: yes, J: no | + +----------------------+----------------------+----------------------+---------------------+--------------+------------------------------------------+------------------------------+------------------------+ - *Sequential Test Runs*: ``Artemis`` can generate a build plan which first executes structural and then behavioral tests. This feature can help students to better concentrate on the immediate challenge at hand. - *Static Code Analysis*: ``Artemis`` can generate a build plan which additionally executes static code analysis tools. diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingLanguage.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingLanguage.java index fe6a36d2eed9..5f4a1451720a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingLanguage.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingLanguage.java @@ -52,6 +52,7 @@ public enum ProgrammingLanguage { OCAML, PYTHON, R, + RUBY, RUST, SWIFT, TYPESCRIPT, diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/TemplateUpgradePolicyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/TemplateUpgradePolicyService.java index d066241c7211..baad5929c94e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/TemplateUpgradePolicyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/TemplateUpgradePolicyService.java @@ -32,9 +32,9 @@ public TemplateUpgradePolicyService(JavaTemplateUpgradeService javaRepositoryUpg public TemplateUpgradeService getUpgradeService(ProgrammingLanguage programmingLanguage) { return switch (programmingLanguage) { case JAVA -> javaRepositoryUpgradeService; - case KOTLIN, PYTHON, C, HASKELL, VHDL, ASSEMBLER, SWIFT, OCAML, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS, TYPESCRIPT, C_SHARP, GO, BASH, MATLAB -> + case KOTLIN, PYTHON, C, HASKELL, VHDL, ASSEMBLER, SWIFT, OCAML, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS, TYPESCRIPT, C_SHARP, GO, BASH, MATLAB, RUBY -> defaultRepositoryUpgradeService; - case SQL, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + programmingLanguage); + case SQL, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + programmingLanguage); }; } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ci/ContinuousIntegrationService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ci/ContinuousIntegrationService.java index c03b64a6022c..01ff763c0154 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ci/ContinuousIntegrationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ci/ContinuousIntegrationService.java @@ -219,9 +219,9 @@ enum RepositoryCheckoutPath implements CustomizableCheckoutPath { @Override public String forProgrammingLanguage(ProgrammingLanguage language) { return switch (language) { - case JAVA, PYTHON, C, HASKELL, KOTLIN, VHDL, ASSEMBLER, SWIFT, OCAML, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS, TYPESCRIPT, C_SHARP, GO, BASH, MATLAB -> + case JAVA, PYTHON, C, HASKELL, KOTLIN, VHDL, ASSEMBLER, SWIFT, OCAML, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS, TYPESCRIPT, C_SHARP, GO, BASH, MATLAB, RUBY -> "assignment"; - case SQL, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + language); + case SQL, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + language); }; } }, @@ -231,8 +231,8 @@ public String forProgrammingLanguage(ProgrammingLanguage language) { public String forProgrammingLanguage(ProgrammingLanguage language) { return switch (language) { case JAVA, PYTHON, HASKELL, KOTLIN, SWIFT, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS, TYPESCRIPT -> ""; - case C, VHDL, ASSEMBLER, OCAML, C_SHARP, GO, BASH, MATLAB -> "tests"; - case SQL, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + language); + case C, VHDL, ASSEMBLER, OCAML, C_SHARP, GO, BASH, MATLAB, RUBY -> "tests"; + case SQL, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + language); }; } }, diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/JenkinsProgrammingLanguageFeatureService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/JenkinsProgrammingLanguageFeatureService.java index eb8a47580c43..74cd0bd1e183 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/JenkinsProgrammingLanguageFeatureService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/JenkinsProgrammingLanguageFeatureService.java @@ -13,6 +13,7 @@ import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.KOTLIN; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.PYTHON; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.R; +import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.RUBY; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.RUST; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.SWIFT; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.TYPESCRIPT; @@ -62,6 +63,7 @@ protected Map getSupportedProgr programmingLanguageFeatures.put(KOTLIN, new ProgrammingLanguageFeature(KOTLIN, true, false, true, true, false, List.of(), false)); programmingLanguageFeatures.put(PYTHON, new ProgrammingLanguageFeature(PYTHON, false, false, true, false, false, List.of(), false)); programmingLanguageFeatures.put(R, new ProgrammingLanguageFeature(R, false, false, true, false, false, List.of(), false)); + programmingLanguageFeatures.put(RUBY, new ProgrammingLanguageFeature(RUBY, false, false, false, false, false, List.of(), false)); programmingLanguageFeatures.put(RUST, new ProgrammingLanguageFeature(RUST, false, false, true, false, false, List.of(), false)); // Jenkins is not supporting XCODE at the moment programmingLanguageFeatures.put(SWIFT, new ProgrammingLanguageFeature(SWIFT, false, true, true, true, false, List.of(PLAIN), false)); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/build_plan/JenkinsBuildPlanService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/build_plan/JenkinsBuildPlanService.java index 970ffb719c51..b639d6ae34d2 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/build_plan/JenkinsBuildPlanService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/build_plan/JenkinsBuildPlanService.java @@ -172,8 +172,8 @@ private JenkinsXmlConfigBuilder builderFor(ProgrammingLanguage programmingLangua throw new UnsupportedOperationException("Xcode templates are not available for Jenkins."); } return switch (programmingLanguage) { - case JAVA, KOTLIN, PYTHON, C, HASKELL, SWIFT, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS, TYPESCRIPT, C_SHARP, GO, BASH -> jenkinsBuildPlanCreator; - case VHDL, ASSEMBLER, OCAML, SQL, MATLAB, RUBY, POWERSHELL, ADA, DART, PHP -> + case JAVA, KOTLIN, PYTHON, C, HASKELL, SWIFT, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS, TYPESCRIPT, C_SHARP, GO, BASH, RUBY -> jenkinsBuildPlanCreator; + case VHDL, ASSEMBLER, OCAML, SQL, MATLAB, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException(programmingLanguage + " templates are not available for Jenkins."); }; } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIProgrammingLanguageFeatureService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIProgrammingLanguageFeatureService.java index 41cdabd90bda..3a10c46e8f9a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIProgrammingLanguageFeatureService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIProgrammingLanguageFeatureService.java @@ -16,6 +16,7 @@ import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.OCAML; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.PYTHON; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.R; +import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.RUBY; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.RUST; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.SWIFT; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.TYPESCRIPT; @@ -71,6 +72,7 @@ protected Map getSupportedProgr programmingLanguageFeatures.put(OCAML, new ProgrammingLanguageFeature(OCAML, false, false, false, false, true, List.of(), true)); programmingLanguageFeatures.put(PYTHON, new ProgrammingLanguageFeature(PYTHON, false, true, true, false, false, List.of(), true)); programmingLanguageFeatures.put(R, new ProgrammingLanguageFeature(R, false, false, true, false, false, List.of(), true)); + programmingLanguageFeatures.put(RUBY, new ProgrammingLanguageFeature(RUBY, false, false, false, false, false, List.of(), true)); programmingLanguageFeatures.put(RUST, new ProgrammingLanguageFeature(RUST, false, false, true, false, false, List.of(), true)); programmingLanguageFeatures.put(SWIFT, new ProgrammingLanguageFeature(SWIFT, false, false, true, true, false, List.of(PLAIN), true)); programmingLanguageFeatures.put(TYPESCRIPT, new ProgrammingLanguageFeature(TYPESCRIPT, false, false, true, false, false, List.of(), true)); diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml index 5e4b7713fe50..04634254d4ac 100644 --- a/src/main/resources/config/application.yml +++ b/src/main/resources/config/application.yml @@ -107,6 +107,8 @@ artemis: default: "ghcr.io/ls1intum/artemis-go-docker:v1.0.0" matlab: default: "mathworks/matlab:r2024b" + ruby: + default: "ghcr.io/ls1intum/artemis-ruby-docker:v1.0.0" # The following properties are used to configure the Artemis build agent. # The build agent is responsible for executing the buildJob to test student submissions. diff --git a/src/main/resources/templates/aeolus/ruby/default.sh b/src/main/resources/templates/aeolus/ruby/default.sh new file mode 100644 index 000000000000..eca5b3e5156c --- /dev/null +++ b/src/main/resources/templates/aeolus/ruby/default.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -e + +test () { + echo '⚙️ executing test' + cd "${testWorkingDirectory}" + bundler exec rake ci:test +} + +main () { + test +} + +main "${@}" diff --git a/src/main/resources/templates/aeolus/ruby/default.yaml b/src/main/resources/templates/aeolus/ruby/default.yaml new file mode 100644 index 000000000000..bd32a925c8e4 --- /dev/null +++ b/src/main/resources/templates/aeolus/ruby/default.yaml @@ -0,0 +1,13 @@ +api: v0.0.1 +metadata: + name: "Ruby" + id: ruby +actions: + - name: test + script: |- + cd "${testWorkingDirectory}" + bundler exec rake ci:test + results: + - name: Minitest Test Results + path: "${testWorkingDirectory}/report.xml" + type: junit diff --git a/src/main/resources/templates/jenkins/ruby/regularRuns/pipeline.groovy b/src/main/resources/templates/jenkins/ruby/regularRuns/pipeline.groovy new file mode 100644 index 000000000000..b3afb23cdf64 --- /dev/null +++ b/src/main/resources/templates/jenkins/ruby/regularRuns/pipeline.groovy @@ -0,0 +1,55 @@ +/* + * This file configures the actual build steps for the automatic grading. + * + * !!! + * For regular exercises, there is no need to make changes to this file. + * Only this base configuration is actively supported by the Artemis maintainers + * and/or your Artemis instance administrators. + * !!! + */ + +dockerImage = '#dockerImage' +dockerFlags = '#dockerArgs' + +/** + * Main function called by Jenkins. + */ +void testRunner() { + docker.image(dockerImage).inside(dockerFlags) { c -> + runTestSteps() + } +} + +private void runTestSteps() { + test() +} + +/** + * Run unit tests + */ +private void test() { + stage('Test') { + sh ''' + cd tests + bundler exec rake ci:test + ''' + } +} + +/** + * Script of the post build tasks aggregating all JUnit files in $WORKSPACE/results. + * + * Called by Jenkins. + */ +void postBuildTasks() { + sh ''' + rm -rf results + mkdir results + cp tests/report.xml $WORKSPACE/results/ || true + sed -i 's/[^[:print:]\t]/�/g' $WORKSPACE/results/*.xml || true + ''' +} + +// very important, do not remove +// required so that Jenkins finds the methods defined in this script +return this diff --git a/src/main/resources/templates/ruby/exercise/.gitignore b/src/main/resources/templates/ruby/exercise/.gitignore new file mode 100644 index 000000000000..9106b2a345b0 --- /dev/null +++ b/src/main/resources/templates/ruby/exercise/.gitignore @@ -0,0 +1,8 @@ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ diff --git a/src/main/resources/templates/ruby/exercise/Gemfile b/src/main/resources/templates/ruby/exercise/Gemfile new file mode 100644 index 000000000000..dba00cea4087 --- /dev/null +++ b/src/main/resources/templates/ruby/exercise/Gemfile @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gem "date", "~> 3.4" diff --git a/src/main/resources/templates/ruby/exercise/Gemfile.lock b/src/main/resources/templates/ruby/exercise/Gemfile.lock new file mode 100644 index 000000000000..af596da80a54 --- /dev/null +++ b/src/main/resources/templates/ruby/exercise/Gemfile.lock @@ -0,0 +1,14 @@ +GEM + remote: https://rubygems.org/ + specs: + date (3.4.1) + +PLATFORMS + ruby + x86_64-linux + +DEPENDENCIES + date (~> 3.4) + +BUNDLED WITH + 2.6.2 diff --git a/src/main/resources/templates/ruby/exercise/src/bubble_sort.rb b/src/main/resources/templates/ruby/exercise/src/bubble_sort.rb new file mode 100644 index 000000000000..5b4f9744ba04 --- /dev/null +++ b/src/main/resources/templates/ruby/exercise/src/bubble_sort.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class BubbleSort + # Sorts dates with BubbleSort. + # @param input [Array] the Array of Dates to be sorted + def perform_sort(input) + # TODO: implement + + raise(NotImplementedError) + end +end diff --git a/src/main/resources/templates/ruby/exercise/src/client.rb b/src/main/resources/templates/ruby/exercise/src/client.rb new file mode 100755 index 000000000000..697afa7fc52c --- /dev/null +++ b/src/main/resources/templates/ruby/exercise/src/client.rb @@ -0,0 +1,66 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "date" + +module Client + # TODO: Implement BubbleSort + # TODO: Implement MergeSort + + # TODO: Create and implement a Context class according to the UML class diagram + # TODO: Create and implement a Policy class as described in the problem statement + + ITERATIONS = 10 + DATES_LENGTH_MIN = 5 + DATES_LENGTH_MAX = 15 + + # Main method. + # Add code to demonstrate your implementation here. + def self.main + # TODO: Init Context and Policy + + # Run multiple times to simulate different sorting strategies + ITERATIONS.times do + dates = create_random_dates + + # TODO: Configure context + + print("Unsorted Array of dates: ") + print_dates(dates) + + # TODO: Sort dates + + print("Sorted Array of dates: ") + print_dates(dates) + end + end + + # Generates a List of random Date objects with random List size between + # DATES_LENGTH_MIN and DATES_LENGTH_MAX. + # @return [Array] an Array of random Date objects. + def self.create_random_dates + dates_length = Random.rand(DATES_LENGTH_MIN..DATES_LENGTH_MAX) + + lowest_date = Date.new(2024, 9, 15) + highest_date = Date.new(2025, 1, 15) + + Array.new(dates_length) { random_date_within(lowest_date, highest_date) } + end + + # Creates a random Date within the given range. + # @param low [Date] the lower bound + # @param high [Date] the upper bound + # @return [Date] a random Date within the given range + def self.random_date_within(low, high) + random_jd = Random.rand(low.jd..high.jd) + Date.jd(random_jd) + end + + # Prints out the given Array of Date objects. + # @param dates [Array] list of the dates to print + def self.print_dates(dates) + puts(dates.join(", ")) + end +end + +Client.main diff --git a/src/main/resources/templates/ruby/exercise/src/merge_sort.rb b/src/main/resources/templates/ruby/exercise/src/merge_sort.rb new file mode 100644 index 000000000000..25a227ed89c3 --- /dev/null +++ b/src/main/resources/templates/ruby/exercise/src/merge_sort.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class MergeSort + # Sorts dates with MergeSort. + # @param input [Array] the Array of Dates to be sorted + def perform_sort(input) + # TODO: implement + + raise(NotImplementedError) + end +end diff --git a/src/main/resources/templates/ruby/readme b/src/main/resources/templates/ruby/readme new file mode 100644 index 000000000000..ee6e5c451431 --- /dev/null +++ b/src/main/resources/templates/ruby/readme @@ -0,0 +1,89 @@ +# Sorting with the Strategy Pattern + +In this exercise, we want to implement sorting algorithms and choose them based on runtime specific variables. + +### Part 1: Sorting + +First, we need to implement two sorting algorithms, in this case `MergeSort` and `BubbleSort`. + +**You have the following tasks:** + +1. [task][Implement Bubble Sort](test_bubble_sort_sorts) +Implement the method `perform_sort(input: Array[Date])` in the class `BubbleSort`. Make sure to follow the Bubble Sort algorithm exactly. + +2. [task][Implement Merge Sort](test_merge_sort_sorts) +Implement the method `perform_sort(input: Array[Date])` in the class `MergeSort`. Make sure to follow the Merge Sort algorithm exactly. + +### Part 2: Strategy Pattern + +We want the application to apply different algorithms for sorting an `Array` of `Date` objects. +Use the strategy pattern to select the right sorting algorithm at runtime. + +**You have the following tasks:** + +1. [task][Context Class](test_context_structure) +Create and implement a `Context` class in `context.rb` following the class diagram below. +Add read and write accessors for the attributes and associations. + +2. [task][Context Policy](test_policy_structure) +Create and implement a `Policy` class in `policy.rb` following the class diagram below. +Add read and write accessors for the attributes and associations. +`Policy` should implement a simple configuration mechanism: + + 1. [task][Select MergeSort](test_use_merge_sort_for_big_list) + Select `MergeSort` when the Array has more than 10 dates. + + 2. [task][Select BubbleSort](test_use_bubble_sort_for_small_list) + Select `BubbleSort` when the Array has less or equal 10 dates. + +3. Complete the `Client` class which demonstrates switching between two strategies at runtime. + +@startuml + +class Client { +} + +class Policy ##testsColor(test_policy_structure) { + +configure() +} + +class Context ##testsColor(test_context_structure) { + -dates: Array[Date] + +sort() +} + +interface SortStrategy { + +perform_sort(input: Array[Date]) +} + +class BubbleSort { + +perform_sort(input: Array[Date]) +} + +class MergeSort { + +perform_sort(input: Array[Date]) +} + +MergeSort -up-|> SortStrategy +BubbleSort -up-|> SortStrategy +Policy -right-> Context #testsColor(test_policy_structure): context +Context -right-> SortStrategy #testsColor(test_context_structure): sort_algorithm +Client .down.> Policy +Client .down.> Context + +hide empty fields +hide empty methods + +@enduml + + +### Part 3: Optional Challenges + +(These are not tested) + +1. Create a new class `QuickSort` and implement the Quick Sort algorithm. + +2. Make the method `perform_sort(input: Array[Date])` not depend on `Date` objects, so that other objects can also be sorted by the same method. +**Hint:** Have a look at the module `Comparable`. + +3. Think about a useful decision in `Policy` when to use the new `QuickSort` algorithm. diff --git a/src/main/resources/templates/ruby/solution/.gitignore b/src/main/resources/templates/ruby/solution/.gitignore new file mode 100644 index 000000000000..9106b2a345b0 --- /dev/null +++ b/src/main/resources/templates/ruby/solution/.gitignore @@ -0,0 +1,8 @@ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ diff --git a/src/main/resources/templates/ruby/solution/Gemfile b/src/main/resources/templates/ruby/solution/Gemfile new file mode 100644 index 000000000000..dba00cea4087 --- /dev/null +++ b/src/main/resources/templates/ruby/solution/Gemfile @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gem "date", "~> 3.4" diff --git a/src/main/resources/templates/ruby/solution/Gemfile.lock b/src/main/resources/templates/ruby/solution/Gemfile.lock new file mode 100644 index 000000000000..af596da80a54 --- /dev/null +++ b/src/main/resources/templates/ruby/solution/Gemfile.lock @@ -0,0 +1,14 @@ +GEM + remote: https://rubygems.org/ + specs: + date (3.4.1) + +PLATFORMS + ruby + x86_64-linux + +DEPENDENCIES + date (~> 3.4) + +BUNDLED WITH + 2.6.2 diff --git a/src/main/resources/templates/ruby/solution/src/bubble_sort.rb b/src/main/resources/templates/ruby/solution/src/bubble_sort.rb new file mode 100644 index 000000000000..ae9a7b5b8da5 --- /dev/null +++ b/src/main/resources/templates/ruby/solution/src/bubble_sort.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class BubbleSort + # Sorts dates with BubbleSort. + # @param input [Array] the Array of Dates to be sorted + def perform_sort(input) + (input.length - 1).downto(0) do |i| + (0...i).each do |j| + input[j], input[j + 1] = input[j + 1], input[j] if input[j] > input[j + 1] + end + end + end +end diff --git a/src/main/resources/templates/ruby/solution/src/client.rb b/src/main/resources/templates/ruby/solution/src/client.rb new file mode 100755 index 000000000000..695fbc348da9 --- /dev/null +++ b/src/main/resources/templates/ruby/solution/src/client.rb @@ -0,0 +1,64 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "date" + +require_relative "context" +require_relative "policy" + +module Client + ITERATIONS = 10 + DATES_LENGTH_MIN = 5 + DATES_LENGTH_MAX = 15 + + # Main method. + # Add code to demonstrate your implementation here. + def self.main + context = Context.new + policy = Policy.new(context) + + ITERATIONS.times do + dates = create_random_dates + + context.dates = dates + policy.configure + + print("Unsorted Array of dates: ") + print_dates(dates) + + context.sort + + print("Sorted Array of dates: ") + print_dates(dates) + end + end + + # Generates a List of random Date objects with random List size between + # DATES_LENGTH_MIN and DATES_LENGTH_MAX. + # @return [Array] an Array of random Date objects. + def self.create_random_dates + dates_length = Random.rand(DATES_LENGTH_MIN..DATES_LENGTH_MAX) + + lowest_date = Date.new(2024, 9, 15) + highest_date = Date.new(2025, 1, 15) + + Array.new(dates_length) { random_date_within(lowest_date, highest_date) } + end + + # Creates a random Date within the given range. + # @param low [Date] the lower bound + # @param high [Date] the upper bound + # @return [Date] a random Date within the given range + def self.random_date_within(low, high) + random_jd = Random.rand(low.jd..high.jd) + Date.jd(random_jd) + end + + # Prints out the given Array of Date objects. + # @param dates [Array] list of the dates to print + def self.print_dates(dates) + puts(dates.join(", ")) + end +end + +Client.main diff --git a/src/main/resources/templates/ruby/solution/src/context.rb b/src/main/resources/templates/ruby/solution/src/context.rb new file mode 100644 index 000000000000..09a51bdc6861 --- /dev/null +++ b/src/main/resources/templates/ruby/solution/src/context.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class Context + attr_accessor :dates, :sort_algorithm + + # Runs the configured sort algorithm. + def sort + raise "sort_algorithm not set" if @sort_algorithm.nil? + raise "dates not set" if @dates.nil? + + @sort_algorithm.perform_sort(@dates) + end +end diff --git a/src/main/resources/templates/ruby/solution/src/merge_sort.rb b/src/main/resources/templates/ruby/solution/src/merge_sort.rb new file mode 100644 index 000000000000..7e2b70286d46 --- /dev/null +++ b/src/main/resources/templates/ruby/solution/src/merge_sort.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +class MergeSort + # Wrapper method for the real MergeSort algorithm. + # @param input [Array] the Array of Dates to be sorted + def perform_sort(input) + mergesort(input, 0, input.length) + end + + private + + # Recursive merge sort method + def mergesort(input, low, high) + return if high - low <= 1 + + mid = (low + high) / 2 + + mergesort(input, low, mid) + mergesort(input, mid, high) + merge(input, low, mid, high) + end + + # Merge method + def merge(input, low, middle, high) + temp = Array.new(high - low) + + left_index = low + right_index = middle + whole_index = 0 + + while left_index < middle && right_index < high + if input[left_index] <= input[right_index] + temp[whole_index] = input[left_index] + left_index += 1 + else + temp[whole_index] = input[right_index] + right_index += 1 + end + whole_index += 1 + end + + while left_index < middle + temp[whole_index] = input[left_index] + left_index += 1 + whole_index += 1 + end + + while right_index < high + temp[whole_index] = input[right_index] + right_index += 1 + whole_index += 1 + end + + temp.each_with_index do |value, index| + input[low + index] = value + end + end +end diff --git a/src/main/resources/templates/ruby/solution/src/policy.rb b/src/main/resources/templates/ruby/solution/src/policy.rb new file mode 100644 index 000000000000..c8ba20b4105c --- /dev/null +++ b/src/main/resources/templates/ruby/solution/src/policy.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require_relative "bubble_sort" +require_relative "merge_sort" + +class Policy + attr_accessor :context + + DATES_SIZE_THRESHOLD = 10 + + def initialize(context) + @context = context + end + + # Chooses a strategy depending on the number of date objects. + def configure + sort_algorithm = if @context.dates.length > DATES_SIZE_THRESHOLD + MergeSort.new + else + BubbleSort.new + end + @context.sort_algorithm = sort_algorithm + end +end diff --git a/src/main/resources/templates/ruby/test/.gitignore b/src/main/resources/templates/ruby/test/.gitignore new file mode 100644 index 000000000000..f7edf22658b3 --- /dev/null +++ b/src/main/resources/templates/ruby/test/.gitignore @@ -0,0 +1,11 @@ +/report.xml +/rubocop.sarif + +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ diff --git a/src/main/resources/templates/ruby/test/Gemfile b/src/main/resources/templates/ruby/test/Gemfile new file mode 100644 index 000000000000..98ac822b9f28 --- /dev/null +++ b/src/main/resources/templates/ruby/test/Gemfile @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gem "rake", "~> 13.0" + +gem "minitest", "~> 5.16" +gem "minitest-junit", "~> 2.1" + +gem "date", "~> 3.4" diff --git a/src/main/resources/templates/ruby/test/Gemfile.lock b/src/main/resources/templates/ruby/test/Gemfile.lock new file mode 100644 index 000000000000..761adfe3c6f4 --- /dev/null +++ b/src/main/resources/templates/ruby/test/Gemfile.lock @@ -0,0 +1,25 @@ +GEM + remote: https://rubygems.org/ + specs: + bigdecimal (3.1.9) + date (3.4.1) + minitest (5.25.4) + minitest-junit (2.1.0) + minitest (~> 5.11) + ox (~> 2, >= 2.14.2) + ox (2.14.20) + bigdecimal (>= 3.0) + rake (13.2.1) + +PLATFORMS + ruby + x86_64-linux + +DEPENDENCIES + date (~> 3.4) + minitest (~> 5.16) + minitest-junit (~> 2.1) + rake (~> 13.0) + +BUNDLED WITH + 2.6.2 diff --git a/src/main/resources/templates/ruby/test/Rakefile b/src/main/resources/templates/ruby/test/Rakefile new file mode 100644 index 000000000000..1066ffc918ca --- /dev/null +++ b/src/main/resources/templates/ruby/test/Rakefile @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require_relative "assignment_path" + +require "minitest/test_task" + +Minitest::TestTask.create :test + +namespace :ci do + Minitest::TestTask.create :test do |task| + task.extra_args = ["--verbose", "--junit", "--junit-jenkins", "--junit-filename=report.xml"] + end +end + +task default: %i[test] +task ci: %i[ci:test] diff --git a/src/main/resources/templates/ruby/test/assignment_path.rb b/src/main/resources/templates/ruby/test/assignment_path.rb new file mode 100644 index 000000000000..55e8c0b163ab --- /dev/null +++ b/src/main/resources/templates/ruby/test/assignment_path.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +ASSIGNMENT_PATH = File.expand_path("../${studentParentWorkingDirectoryName}", __dir__) diff --git a/src/main/resources/templates/ruby/test/test/test_behavior.rb b/src/main/resources/templates/ruby/test/test/test_behavior.rb new file mode 100644 index 000000000000..0c76fcf42b74 --- /dev/null +++ b/src/main/resources/templates/ruby/test/test/test_behavior.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +require "date" + +class TestBehavior < Minitest::Test + def setup + @dates = [ + Date.new(2018, 11, 8), + Date.new(2017, 4, 15), + Date.new(2016, 2, 15), + Date.new(2017, 9, 15) + ] + @ordered_dates = [ + Date.new(2016, 2, 15), + Date.new(2017, 4, 15), + Date.new(2017, 9, 15), + Date.new(2018, 11, 8) + ] + end + + def test_bubble_sort_sorts + require "bubble_sort" + + bubble_sort = BubbleSort.new + bubble_sort.perform_sort(@dates) + assert_equal(@ordered_dates, @dates, "dates were not sorted") + end + + def test_merge_sort_sorts + require "merge_sort" + + merge_sort = MergeSort.new + merge_sort.perform_sort(@dates) + assert_equal(@ordered_dates, @dates, "dates were not sorted") + end + + def test_use_merge_sort_for_big_list + require "context" + require "policy" + require "bubble_sort" + + dates = Array.new(11, 0) + context = Context.new + context.dates = dates + policy = Policy.new(context) + policy.configure + assert(context.sort_algorithm.instance_of?(MergeSort), "selected algorithm was not MergeSort") + end + + def test_use_bubble_sort_for_small_list + require "context" + require "policy" + require "merge_sort" + + dates = Array.new(3, 0) + context = Context.new + context.dates = dates + policy = Policy.new(context) + policy.configure + assert(context.sort_algorithm.instance_of?(BubbleSort), "selected algorithm was not BubbleSort") + end +end diff --git a/src/main/resources/templates/ruby/test/test/test_helper.rb b/src/main/resources/templates/ruby/test/test/test_helper.rb new file mode 100644 index 000000000000..8ab2ec04aeb9 --- /dev/null +++ b/src/main/resources/templates/ruby/test/test/test_helper.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require_relative "../assignment_path" + +$LOAD_PATH.unshift File.join(ASSIGNMENT_PATH, "src") + +require "minitest/autorun" + +# exit successfully on failure +Minitest.after_run do + exit 0 +end diff --git a/src/main/resources/templates/ruby/test/test/test_structural.rb b/src/main/resources/templates/ruby/test/test/test_structural.rb new file mode 100644 index 000000000000..417522f29efe --- /dev/null +++ b/src/main/resources/templates/ruby/test/test/test_structural.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +class TestStructural < Minitest::Test + def test_context_structure + require "context" + + assert(defined?(Context), "Context is not defined") + assert(Context.instance_of?(Class), "Context is not a class") + assert_equal([], Context.instance_method(:initialize).parameters, + "The constructor of Context does not have the expected parameters") + assert(Context.public_method_defined?(:dates), "Context has not attribute reader for 'dates'") + assert(Context.public_method_defined?(:dates=), "Context has not attribute writer for 'dates'") + assert(Context.public_method_defined?(:sort_algorithm), "Context has not attribute reader for 'sort_algorithm'") + assert(Context.public_method_defined?(:sort_algorithm=), "Context has not attribute writer for 'sort_algorithm'") + assert(Context.public_method_defined?(:sort), "Context has no method 'sort'") + end + + def test_policy_structure + require "policy" + + assert(defined?(Policy), "Policy is not defined") + assert(Policy.instance_of?(Class), "Policy is not a class") + assert_equal([%i[req context]], Policy.instance_method(:initialize).parameters, + "The constructor of Policy does not have the expected parameters") + assert(Policy.public_method_defined?(:context), "Policy has not attribute reader for 'context'") + assert(Policy.public_method_defined?(:context=), "Policy has not attribute writer for 'context'") + assert(Policy.public_method_defined?(:configure), "Policy has no method 'configure'") + end +end diff --git a/src/main/webapp/app/entities/programming/programming-exercise.model.ts b/src/main/webapp/app/entities/programming/programming-exercise.model.ts index 8b5e1b0136b3..b07d272ff26a 100644 --- a/src/main/webapp/app/entities/programming/programming-exercise.model.ts +++ b/src/main/webapp/app/entities/programming/programming-exercise.model.ts @@ -27,6 +27,7 @@ export enum ProgrammingLanguage { OCAML = 'OCAML', PYTHON = 'PYTHON', R = 'R', + RUBY = 'RUBY', RUST = 'RUST', SWIFT = 'SWIFT', TYPESCRIPT = 'TYPESCRIPT', diff --git a/src/test/resources/config/application.yml b/src/test/resources/config/application.yml index 2eca321307c2..43996ae2d62b 100644 --- a/src/test/resources/config/application.yml +++ b/src/test/resources/config/application.yml @@ -84,6 +84,8 @@ artemis: default: "~~invalid~~" matlab: default: "~~invalid~~" + ruby: + default: "~~invalid~~" build-agent: short-name: "artemis-build-agent-test" telemetry: From 7d4f9dcae736d0fbd8793a798d4a51ca92f60923 Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Fri, 31 Jan 2025 08:23:07 +0100 Subject: [PATCH 7/8] Development: Update test coverage --- gradle/jacoco.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/jacoco.gradle b/gradle/jacoco.gradle index a76db0977c6e..d8089206e76f 100644 --- a/gradle/jacoco.gradle +++ b/gradle/jacoco.gradle @@ -42,8 +42,8 @@ ext { "CLASS": 1 ], "iris" : [ - "INSTRUCTION": 0.792, - "CLASS": 17 + "INSTRUCTION": 0.775, + "CLASS": 22 ], "lecture" : [ "INSTRUCTION": 0.867, From 6d861a9755a7700df4c3358e295ad19dd85022c3 Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Fri, 31 Jan 2025 08:28:55 +0100 Subject: [PATCH 8/8] Development: Update client dependencies --- package-lock.json | 539 +++++++++++++++++++++++----------------------- package.json | 42 ++-- 2 files changed, 287 insertions(+), 294 deletions(-) diff --git a/package-lock.json b/package-lock.json index ed48679b10de..044c2ebf8d4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,18 +10,18 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@angular/animations": "19.1.3", - "@angular/cdk": "19.1.1", - "@angular/common": "19.1.3", - "@angular/compiler": "19.1.3", - "@angular/core": "19.1.3", - "@angular/forms": "19.1.3", - "@angular/localize": "19.1.3", - "@angular/material": "19.1.1", - "@angular/platform-browser": "19.1.3", - "@angular/platform-browser-dynamic": "19.1.3", - "@angular/router": "19.1.3", - "@angular/service-worker": "19.1.3", + "@angular/animations": "19.1.4", + "@angular/cdk": "19.1.2", + "@angular/common": "19.1.4", + "@angular/compiler": "19.1.4", + "@angular/core": "19.1.4", + "@angular/forms": "19.1.4", + "@angular/localize": "19.1.4", + "@angular/material": "19.1.2", + "@angular/platform-browser": "19.1.4", + "@angular/platform-browser-dynamic": "19.1.4", + "@angular/router": "19.1.4", + "@angular/service-worker": "19.1.4", "@ctrl/ngx-emoji-mart": "9.2.0", "@danielmoncada/angular-datetime-picker": "19.1.0", "@fingerprintjs/fingerprintjs": "4.5.1", @@ -33,7 +33,7 @@ "@ng-bootstrap/ng-bootstrap": "18.0.0", "@ngx-translate/core": "16.0.4", "@ngx-translate/http-loader": "16.0.1", - "@sentry/angular": "8.51.0", + "@sentry/angular": "8.52.1", "@siemens/ngx-datatable": "22.4.1", "@swimlane/ngx-charts": "21.1.3", "@swimlane/ngx-graph": "9.0.1", @@ -45,7 +45,7 @@ "crypto-js": "4.2.0", "dayjs": "1.11.13", "diff-match-patch-typescript": "1.1.0", - "dompurify": "3.2.3", + "dompurify": "3.2.4", "emoji-js": "3.8.1", "export-to-csv": "1.4.0", "fast-json-patch": "3.1.1", @@ -65,7 +65,7 @@ "ngx-infinite-scroll": "19.0.0", "ngx-webstorage": "19.0.1", "pako": "2.1.0", - "papaparse": "5.5.1", + "papaparse": "5.5.2", "pdf-lib": "1.17.1", "pdfjs-dist": "4.10.38", "rxjs": "7.8.1", @@ -89,11 +89,11 @@ "@angular-eslint/eslint-plugin-template": "19.0.2", "@angular-eslint/schematics": "19.0.2", "@angular-eslint/template-parser": "19.0.2", - "@angular/build": "19.1.4", - "@angular/cli": "19.1.4", - "@angular/compiler-cli": "19.1.3", - "@angular/language-service": "19.1.3", - "@sentry/types": "8.51.0", + "@angular/build": "19.1.5", + "@angular/cli": "19.1.5", + "@angular/compiler-cli": "19.1.4", + "@angular/language-service": "19.1.4", + "@sentry/types": "8.52.1", "@testing-library/angular": "17.3.5", "@types/crypto-js": "4.2.2", "@types/d3-shape": "3.1.7", @@ -102,7 +102,7 @@ "@types/jest": "29.5.14", "@types/lodash-es": "4.17.12", "@types/markdown-it": "14.1.2", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/pako": "2.0.3", "@types/papaparse": "5.3.15", "@types/smoothscroll-polyfill": "0.3.4", @@ -287,13 +287,13 @@ "license": "MIT" }, "node_modules/@angular-devkit/architect": { - "version": "0.1901.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1901.4.tgz", - "integrity": "sha512-EoRTN8p7z0YnqOEIJKKu/NwSsCJxFkyGuZOobz7btnUWwlDqG8CNAhJgtlsOXPihwEkHEkzRIm1feDkWEjCYsA==", + "version": "0.1901.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1901.5.tgz", + "integrity": "sha512-zlRudZx34FkFZnSdaQCjxDleHwbQYNLdBFcLi+FBwt0UXqxmhbEIasK3l/3kCOC3QledrjUzVXgouji+OZ/WGQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.1.4", + "@angular-devkit/core": "19.1.5", "rxjs": "7.8.1" }, "engines": { @@ -303,18 +303,18 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "19.1.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-19.1.4.tgz", - "integrity": "sha512-t8qC26Boz1aAMt2xVKthwEXRqMI4ZVwelxRNfHryLdLTujTaehFt3qbjxukMmRGCWmQObauH0UOvDh3pAA24dQ==", + "version": "19.1.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-19.1.5.tgz", + "integrity": "sha512-ny7ktNOTxaEi6cS3V6XFP5bbJkgiMt3OUNUYLdfdbv4y6wolVlPVHKl+wb4xs6tgbnmx63+e6zGpoDMCRytgcg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1901.4", - "@angular-devkit/build-webpack": "0.1901.4", - "@angular-devkit/core": "19.1.4", - "@angular/build": "19.1.4", + "@angular-devkit/architect": "0.1901.5", + "@angular-devkit/build-webpack": "0.1901.5", + "@angular-devkit/core": "19.1.5", + "@angular/build": "19.1.5", "@babel/core": "7.26.0", "@babel/generator": "7.26.3", "@babel/helper-annotate-as-pure": "7.25.9", @@ -325,7 +325,7 @@ "@babel/preset-env": "7.26.0", "@babel/runtime": "7.26.0", "@discoveryjs/json-ext": "0.6.3", - "@ngtools/webpack": "19.1.4", + "@ngtools/webpack": "19.1.5", "@vitejs/plugin-basic-ssl": "1.2.0", "ansi-colors": "4.1.3", "autoprefixer": "10.4.20", @@ -379,7 +379,7 @@ "@angular/localize": "^19.0.0", "@angular/platform-server": "^19.0.0", "@angular/service-worker": "^19.0.0", - "@angular/ssr": "^19.1.4", + "@angular/ssr": "^19.1.5", "@web/test-runner": "^0.19.0", "browser-sync": "^3.0.2", "jest": "^29.5.0", @@ -387,7 +387,7 @@ "karma": "^6.3.0", "ng-packagr": "^19.0.0", "protractor": "^7.0.0", - "tailwindcss": "^2.0.0 || ^3.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", "typescript": ">=5.5 <5.8" }, "peerDependenciesMeta": { @@ -452,14 +452,14 @@ } }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1901.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1901.4.tgz", - "integrity": "sha512-C/Cd1JeRTy2P/powIldc5UZObw92TDGATD/LFlfPfi94celLa2DlEL1ybPTpnGs/R5/q5R26F6fbhmAVSeTJ8g==", + "version": "0.1901.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1901.5.tgz", + "integrity": "sha512-UxEoF7F8L1GpH/N4me7VGe5ZPfxIiVHyhw5/ck3rcVbT6YD22/GYFGSJRGYP+D7LLTJ7OOQvfD6Bc/q62HhWvA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@angular-devkit/architect": "0.1901.4", + "@angular-devkit/architect": "0.1901.5", "rxjs": "7.8.1" }, "engines": { @@ -473,9 +473,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "19.1.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.1.4.tgz", - "integrity": "sha512-IDvSSiQgaixH2RtZtIpq1+XaHeuzMiTWfDyNF9DuYcU+S8CdG1SWrc8d59tmOrM/q+IRGyFgbBhTU1un52hNHw==", + "version": "19.1.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.1.5.tgz", + "integrity": "sha512-wGKV+i5mCM/Hd/3CsdrIYcVi5G2Wg/D5941bUDXivrbsqHfKVINxAkI3OI1eaD90VnAL8ICrQEoAhh6ni2Umkg==", "dev": true, "license": "MIT", "dependencies": { @@ -501,13 +501,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "19.1.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.1.4.tgz", - "integrity": "sha512-EKXBkx6EDcvyO+U68w/eXicRaF92zSSzYNvR3tMZszEKYE6xBr3kZxY99PP54HXQHR4zYwLvFJVp+T6bnvte2w==", + "version": "19.1.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.1.5.tgz", + "integrity": "sha512-8QjOlO2CktcTT0TWcaABea2xSePxoPKaZu96+6gc8oZzj/y8DbdGiO9mRvIac9+m4hiZI41Cqm1W+yMsCzYMkA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.1.4", + "@angular-devkit/core": "19.1.5", "jsonc-parser": "3.3.1", "magic-string": "0.30.17", "ora": "5.4.1", @@ -623,9 +623,9 @@ } }, "node_modules/@angular/animations": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-19.1.3.tgz", - "integrity": "sha512-MI+Tbp9OOisrQtTQH7o+xiQCODXicCs8WHNpGzdCpnXdRkQuVSOb6xAjD9OXJqcQGotLgeyennnkIJGXdz4RTA==", + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-19.1.4.tgz", + "integrity": "sha512-QGswsf/X+k7TijIgBzL6V8+KcArFAgebY6zM0L/Len8v5PNzPzdjJH99+P++5AOLiJctYKfISUwnlMbDb50NrA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -634,18 +634,19 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "19.1.3" + "@angular/core": "19.1.4" } }, "node_modules/@angular/build": { - "version": "19.1.4", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.1.4.tgz", - "integrity": "sha512-yfvLeUT2a8JTuVBY259vsSv0uLyhikHHgQcWa3VSr0TvCKrwCsBIFDq7vqmhLqIVWi/Z4D7n3J5JQAbDrl38Sg==", + "version": "19.1.5", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.1.5.tgz", + "integrity": "sha512-byoHcv0/s6WGWap59s43N/eC+4NsviuTnGoj+iR0ayubk8snn6jdkZLbFDfnTuQlTiu4ok8/XcksjzeMkgGyyw==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1901.4", + "@angular-devkit/architect": "0.1901.5", + "@angular-devkit/core": "19.1.5", "@babel/core": "7.26.0", "@babel/helper-annotate-as-pure": "7.25.9", "@babel/helper-split-export-declaration": "7.24.7", @@ -667,7 +668,7 @@ "rollup": "4.30.1", "sass": "1.83.1", "semver": "7.6.3", - "vite": "6.0.7", + "vite": "6.0.11", "watchpack": "2.4.2" }, "engines": { @@ -684,11 +685,11 @@ "@angular/localize": "^19.0.0", "@angular/platform-server": "^19.0.0", "@angular/service-worker": "^19.0.0", - "@angular/ssr": "^19.1.4", + "@angular/ssr": "^19.1.5", "less": "^4.2.0", "ng-packagr": "^19.0.0", "postcss": "^8.4.0", - "tailwindcss": "^2.0.0 || ^3.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", "typescript": ">=5.5 <5.8" }, "peerDependenciesMeta": { @@ -740,9 +741,9 @@ } }, "node_modules/@angular/cdk": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.1.1.tgz", - "integrity": "sha512-MmfNB9iANuDN1TS+HL8uKqA3/7pdVeCRN+HdAcfqFrcqZmSUUSlYWy8PXqymmyeXxoSwt9p4I/6R0By03VoCMw==", + "version": "19.1.2", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.1.2.tgz", + "integrity": "sha512-rzrZ4BkGNIZWSdw0OsuSB/H9UB5ppPvmBq+uRHdYmZoYjo5wu1pmePxAIZDIBR8xdaNy9rZ4ecS6IebDkgYPrg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -757,18 +758,18 @@ } }, "node_modules/@angular/cli": { - "version": "19.1.4", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.1.4.tgz", - "integrity": "sha512-C1Z2OTLjUJIkLsay6RJ1rzY0Tdb1Mj/cBh9dZryDstuits8G0Tphe36hnLownnoHspFQfjSRtVzF4NwKiDlQRw==", + "version": "19.1.5", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.1.5.tgz", + "integrity": "sha512-bedjH3jUcrLgN3GOTTuvjbPcY3Lm0YcYBVY35S1ugI88UK6nbtttiRdgK++Qk2Q8wbg6zuaBAr4ACbfPMsnRaA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1901.4", - "@angular-devkit/core": "19.1.4", - "@angular-devkit/schematics": "19.1.4", + "@angular-devkit/architect": "0.1901.5", + "@angular-devkit/core": "19.1.5", + "@angular-devkit/schematics": "19.1.5", "@inquirer/prompts": "7.2.1", "@listr2/prompt-adapter-inquirer": "2.0.18", - "@schematics/angular": "19.1.4", + "@schematics/angular": "19.1.5", "@yarnpkg/lockfile": "1.1.0", "ini": "5.0.0", "jsonc-parser": "3.3.1", @@ -791,9 +792,9 @@ } }, "node_modules/@angular/common": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.1.3.tgz", - "integrity": "sha512-r1P0W6FKrON83szIJboF8z6UNCVL4HIxyD+nhmHMMT/iJpu4kDHVugaN/+w2jYLb4oelAJK5xzkzA+1IaHpzLg==", + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.1.4.tgz", + "integrity": "sha512-E4MCl13VIotOxmzKQ/UGciPeaRXQgH7ymesEjYVGcT8jmC+qz5dEcoN7L5Jvq9aUsmLBt9MFp/B5QqKCIXMqYA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -802,14 +803,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "19.1.3", + "@angular/core": "19.1.4", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.1.3.tgz", - "integrity": "sha512-omX5Gyt3zlJVTUteO2YxsqYWtAIpkvs8kRYSUsLTi79V1gbGo+J1TawFuyBTrWxj4UtTGvwmDgZxiCIwMtP5KQ==", + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.1.4.tgz", + "integrity": "sha512-9vGUZ+QhGWvf5dfeILybrh5rvZQtNqS8WumMeX2/vCb0JTA0N4DsL1Sy47HuWcgKBxbmHVUdF5/iufcFaqk2FA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -818,7 +819,7 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "19.1.3" + "@angular/core": "19.1.4" }, "peerDependenciesMeta": { "@angular/core": { @@ -827,9 +828,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.1.3.tgz", - "integrity": "sha512-nDBvZenQECcr9CClmTp3iJNilRQ6oDKFgBkhlWffEFBx0Z6kBA36MXKKLuCkf31D+NGmt5VJlAkl8Ax8BJ9qJw==", + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.1.4.tgz", + "integrity": "sha512-ozJvTUzPOgFqlz69YnV14Ncod+iH0cXZvUKerjw8o+JsixLG2LmJpwQ79Gh4a/ZQmAkAxMAYYK5izCiio8MmTg==", "license": "MIT", "dependencies": { "@babel/core": "7.26.0", @@ -850,14 +851,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "19.1.3", + "@angular/compiler": "19.1.4", "typescript": ">=5.5 <5.8" } }, "node_modules/@angular/core": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.1.3.tgz", - "integrity": "sha512-Hh1eHvi+y+gsTRODiEEEWnRj5zqv9WNoou1KmQ1mv1NTOf0Pv61Hg9P2rBWDr0mPIXFSzqUKjyzW30BgdQ+AEA==", + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.1.4.tgz", + "integrity": "sha512-r3T81lM9evmuW36HA3VAxIJ61M8kirGR8yHoln9fXSnYG8UeJ7JlWEbVRHmVHKOB48VK0bS/VxqN+w9TOq3bZg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -871,9 +872,9 @@ } }, "node_modules/@angular/forms": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.1.3.tgz", - "integrity": "sha512-M6eEJBysJm9zSUhm8ggljZCsgHLccZl70P34tyddb8erh9it2uoOXW0aVaZgDt1UAiF5a1EzjdVdN4TZTT/OGA==", + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.1.4.tgz", + "integrity": "sha512-dcf4G+vXrfvy5NAP+C4A2rBeaZuwKs/TeWjZDpkRUPQMwTvDJcSNH+pqOeVsYUGNY2BkY1uPjzmgZh4F5NMQ9A==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -882,16 +883,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "19.1.3", - "@angular/core": "19.1.3", - "@angular/platform-browser": "19.1.3", + "@angular/common": "19.1.4", + "@angular/core": "19.1.4", + "@angular/platform-browser": "19.1.4", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/language-service": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-19.1.3.tgz", - "integrity": "sha512-6A1Y2j7Qz85GzxKy0C+JFIQaUNoURNR3L6FNvJIfI73ADl74NBy+M+MzZTBlhlfyB3TEGyExZmuV6wHtB/hU+w==", + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-19.1.4.tgz", + "integrity": "sha512-4W6dlBvukL3b7BnGiMM5cPGx3rAAVhBNicfNHX6hXCkz26AV0VFIbfrt/8GRSFmsDYZEOhXvhAy8dxHQCtyCqA==", "dev": true, "license": "MIT", "engines": { @@ -899,9 +900,9 @@ } }, "node_modules/@angular/localize": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-19.1.3.tgz", - "integrity": "sha512-MrDZG3A5Wk9l5AXt9Y5a7FYM608SqqnhWfnWPlkBU8HwE/IEOKY1QDIRLDoUZ1RchEFpI7QX46sqTEIGc5V+hA==", + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-19.1.4.tgz", + "integrity": "sha512-AFfaxnGUWl1QZGmhYNTH8adWynSqjNwHweOUQ/ItIQ+MkbIPOpAtZp+ar6SRJZpatR59O8797jPKVFTAebLvLQ==", "license": "MIT", "dependencies": { "@babel/core": "7.26.0", @@ -918,21 +919,21 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "19.1.3", - "@angular/compiler-cli": "19.1.3" + "@angular/compiler": "19.1.4", + "@angular/compiler-cli": "19.1.4" } }, "node_modules/@angular/material": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-19.1.1.tgz", - "integrity": "sha512-x/EwyBx3yCIYyu/hve19eecmufJzwltRWOO/3Y74jY4jSNNFrR9046t0ptw4fyEXjN8UQZI6Fp/melcZxl3IiA==", + "version": "19.1.2", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-19.1.2.tgz", + "integrity": "sha512-MdczdTmvsXLtEFV9nTgFs9mPM1FSAz4L1kByJCdIgHtOu62e9t2XcSUqOPJTlDrA5wWafZvPWh2woJfJr4iwjw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/animations": "^19.0.0 || ^20.0.0", - "@angular/cdk": "19.1.1", + "@angular/cdk": "19.1.2", "@angular/common": "^19.0.0 || ^20.0.0", "@angular/core": "^19.0.0 || ^20.0.0", "@angular/forms": "^19.0.0 || ^20.0.0", @@ -941,9 +942,9 @@ } }, "node_modules/@angular/platform-browser": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.1.3.tgz", - "integrity": "sha512-bLgnM2hRyzUdoWRoUhe+IMenlr74EvrgwyG7anJ27bjg5PcvhQPXrGqU0hri5yPDb9SHVJZminr7OjNCN8QJkQ==", + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.1.4.tgz", + "integrity": "sha512-IoVIvemj7ni6GLDCvwtZhTgMQjPyG+xPW7rASN2RVl9T3uS1fJUpXrh5JzBcCikIj20O2KV9mqt7p4iIXy9jbQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -952,9 +953,9 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/animations": "19.1.3", - "@angular/common": "19.1.3", - "@angular/core": "19.1.3" + "@angular/animations": "19.1.4", + "@angular/common": "19.1.4", + "@angular/core": "19.1.4" }, "peerDependenciesMeta": { "@angular/animations": { @@ -963,9 +964,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-19.1.3.tgz", - "integrity": "sha512-rfsHu/+wB8YLPjsHKd/Go0SI8zP2gjMkebUHM9SbvVLXEAkxFubcF2htVKbKu8eTncfEJEXD6+3gRAjh5SLrKw==", + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-19.1.4.tgz", + "integrity": "sha512-r1AM8qkjl63cg46tgOHsVV4URHDctcVpt98DU/d/yN8JAugrx6GA1qOM/HMDspMjEIU4aYcSkUUY6h6uIkYmOQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -974,16 +975,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "19.1.3", - "@angular/compiler": "19.1.3", - "@angular/core": "19.1.3", - "@angular/platform-browser": "19.1.3" + "@angular/common": "19.1.4", + "@angular/compiler": "19.1.4", + "@angular/core": "19.1.4", + "@angular/platform-browser": "19.1.4" } }, "node_modules/@angular/router": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-19.1.3.tgz", - "integrity": "sha512-DJ9BgvtxJV6xohaPQXPdBsFCZoQIEq2OPDyKcoW4L0ST4kIIFpHyI6wJ+AlPnLkhSwmOOoHciH0oxZ2xPVxmiQ==", + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-19.1.4.tgz", + "integrity": "sha512-0gEhGGqcCS7adKuv/XeQjRbhEqRXPhIH4ygjwfonV+uvmK+C1sf+bnAt4o01hxwf12w4FcnNPkgBKt+rJJ+LpA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -992,16 +993,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "19.1.3", - "@angular/core": "19.1.3", - "@angular/platform-browser": "19.1.3", + "@angular/common": "19.1.4", + "@angular/core": "19.1.4", + "@angular/platform-browser": "19.1.4", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/service-worker": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-19.1.3.tgz", - "integrity": "sha512-87XPbzqy9sGYkN21y4vqJBT6lMuuQJrkx5ID1pXdDGxUXlfiGprshtbLPNCUcxHXOnLOdnAI2WZ5DDjl4NMNpw==", + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-19.1.4.tgz", + "integrity": "sha512-g4hGvmsVyYbCg/8iFzqi2r4gwOopkYqdTk0NXk+o39OoD2p679uUQ9rZd9oJs1EUMXn08Bk5jhIA/yiaonp3mw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -1013,8 +1014,8 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "19.1.3", - "@angular/core": "19.1.3" + "@angular/core": "19.1.4", + "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@asamuzakjp/css-color": { @@ -3853,15 +3854,15 @@ } }, "node_modules/@inquirer/checkbox": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.0.6.tgz", - "integrity": "sha512-PgP35JfmGjHU0LSXOyRew0zHuA9N6OJwOlos1fZ20b7j8ISeAdib3L+n0jIxBtX958UeEpte6xhG/gxJ5iUqMw==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.0.7.tgz", + "integrity": "sha512-lyoF4uYdBBTnqeB1gjPdYkiQ++fz/iYKaP9DON1ZGlldkvAEJsjaOBRdbl5UW1pOSslBRd701jxhAG0MlhHd2w==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.4", - "@inquirer/figures": "^1.0.9", - "@inquirer/type": "^3.0.2", + "@inquirer/core": "^10.1.5", + "@inquirer/figures": "^1.0.10", + "@inquirer/type": "^3.0.3", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, @@ -3890,19 +3891,18 @@ } }, "node_modules/@inquirer/core": { - "version": "10.1.4", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.4.tgz", - "integrity": "sha512-5y4/PUJVnRb4bwWY67KLdebWOhOc7xj5IP2J80oWXa64mVag24rwQ1VAdnj7/eDY/odhguW0zQ1Mp1pj6fO/2w==", + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.5.tgz", + "integrity": "sha512-/vyCWhET0ktav/mUeBqJRYTwmjFPIKPRYb3COAw7qORULgipGSUO2vL32lQKki3UxDKJ8BvuEbokaoyCA6YlWw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/figures": "^1.0.9", - "@inquirer/type": "^3.0.2", + "@inquirer/figures": "^1.0.10", + "@inquirer/type": "^3.0.3", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", - "strip-ansi": "^6.0.1", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" }, @@ -3911,14 +3911,14 @@ } }, "node_modules/@inquirer/editor": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.3.tgz", - "integrity": "sha512-S9KnIOJuTZpb9upeRSBBhoDZv7aSV3pG9TECrBj0f+ZsFwccz886hzKBrChGrXMJwd4NKY+pOA9Vy72uqnd6Eg==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.4.tgz", + "integrity": "sha512-S8b6+K9PLzxiFGGc02m4syhEu5JsH0BukzRsuZ+tpjJ5aDsDX1WfNfOil2fmsO36Y1RMcpJGxlfQ1yh4WfU28Q==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.4", - "@inquirer/type": "^3.0.2", + "@inquirer/core": "^10.1.5", + "@inquirer/type": "^3.0.3", "external-editor": "^3.1.0" }, "engines": { @@ -3929,14 +3929,14 @@ } }, "node_modules/@inquirer/expand": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.6.tgz", - "integrity": "sha512-TRTfi1mv1GeIZGyi9PQmvAaH65ZlG4/FACq6wSzs7Vvf1z5dnNWsAAXBjWMHt76l+1hUY8teIqJFrWBk5N6gsg==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.7.tgz", + "integrity": "sha512-PsUQ5t7r+DPjW0VVEHzssOTBM2UPHnvBNse7hzuki7f6ekRL94drjjfBLrGEDe7cgj3pguufy/cuFwMeWUWHXw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.4", - "@inquirer/type": "^3.0.2", + "@inquirer/core": "^10.1.5", + "@inquirer/type": "^3.0.3", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -3947,9 +3947,9 @@ } }, "node_modules/@inquirer/figures": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.9.tgz", - "integrity": "sha512-BXvGj0ehzrngHTPTDqUoDT3NXL8U0RxUk2zJm2A66RhCEIWdtU1v6GuUqNAgArW4PQ9CinqIWyHdQgdwOj06zQ==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.10.tgz", + "integrity": "sha512-Ey6176gZmeqZuY/W/nZiUyvmb1/qInjcpiZjXWi6nON+nxJpD1bxtSoBxNliGISae32n6OwbY+TSXPZ1CfS4bw==", "dev": true, "license": "MIT", "engines": { @@ -3957,14 +3957,14 @@ } }, "node_modules/@inquirer/input": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.3.tgz", - "integrity": "sha512-zeo++6f7hxaEe7OjtMzdGZPHiawsfmCZxWB9X1NpmYgbeoyerIbWemvlBxxl+sQIlHC0WuSAG19ibMq3gbhaqQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.4.tgz", + "integrity": "sha512-CKKF8otRBdIaVnRxkFLs00VNA9HWlEh3x4SqUfC3A8819TeOZpTYG/p+4Nqu3hh97G+A0lxkOZNYE7KISgU8BA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.4", - "@inquirer/type": "^3.0.2" + "@inquirer/core": "^10.1.5", + "@inquirer/type": "^3.0.3" }, "engines": { "node": ">=18" @@ -3974,14 +3974,14 @@ } }, "node_modules/@inquirer/number": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.6.tgz", - "integrity": "sha512-xO07lftUHk1rs1gR0KbqB+LJPhkUNkyzV/KhH+937hdkMazmAYHLm1OIrNKpPelppeV1FgWrgFDjdUD8mM+XUg==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.7.tgz", + "integrity": "sha512-uU2nmXGC0kD8+BLgwZqcgBD1jcw2XFww2GmtP6b4504DkOp+fFAhydt7JzRR1TAI2dmj175p4SZB0lxVssNreA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.4", - "@inquirer/type": "^3.0.2" + "@inquirer/core": "^10.1.5", + "@inquirer/type": "^3.0.3" }, "engines": { "node": ">=18" @@ -3991,14 +3991,14 @@ } }, "node_modules/@inquirer/password": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.6.tgz", - "integrity": "sha512-QLF0HmMpHZPPMp10WGXh6F+ZPvzWE7LX6rNoccdktv/Rov0B+0f+eyXkAcgqy5cH9V+WSpbLxu2lo3ysEVK91w==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.7.tgz", + "integrity": "sha512-DFpqWLx+C5GV5zeFWuxwDYaeYnTWYphO07pQ2VnP403RIqRIpwBG0ATWf7pF+3IDbaXEtWatCJWxyDrJ+rkj2A==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.4", - "@inquirer/type": "^3.0.2", + "@inquirer/core": "^10.1.5", + "@inquirer/type": "^3.0.3", "ansi-escapes": "^4.3.2" }, "engines": { @@ -4034,14 +4034,14 @@ } }, "node_modules/@inquirer/rawlist": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.0.6.tgz", - "integrity": "sha512-QoE4s1SsIPx27FO4L1b1mUjVcoHm1pWE/oCmm4z/Hl+V1Aw5IXl8FYYzGmfXaBT0l/sWr49XmNSiq7kg3Kd/Lg==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.0.7.tgz", + "integrity": "sha512-ZeBca+JCCtEIwQMvhuROT6rgFQWWvAImdQmIIP3XoyDFjrp2E0gZlEn65sWIoR6pP2EatYK96pvx0887OATWQQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.4", - "@inquirer/type": "^3.0.2", + "@inquirer/core": "^10.1.5", + "@inquirer/type": "^3.0.3", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -4052,15 +4052,15 @@ } }, "node_modules/@inquirer/search": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.6.tgz", - "integrity": "sha512-eFZ2hiAq0bZcFPuFFBmZEtXU1EarHLigE+ENCtpO+37NHCl4+Yokq1P/d09kUblObaikwfo97w+0FtG/EXl5Ng==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.7.tgz", + "integrity": "sha512-Krq925SDoLh9AWSNee8mbSIysgyWtcPnSAp5YtPBGCQ+OCO+5KGC8FwLpyxl8wZ2YAov/8Tp21stTRK/fw5SGg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.4", - "@inquirer/figures": "^1.0.9", - "@inquirer/type": "^3.0.2", + "@inquirer/core": "^10.1.5", + "@inquirer/figures": "^1.0.10", + "@inquirer/type": "^3.0.3", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -4071,15 +4071,15 @@ } }, "node_modules/@inquirer/select": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.0.6.tgz", - "integrity": "sha512-yANzIiNZ8fhMm4NORm+a74+KFYHmf7BZphSOBovIzYPVLquseTGEkU5l2UTnBOf5k0VLmTgPighNDLE9QtbViQ==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.0.7.tgz", + "integrity": "sha512-ejGBMDSD+Iqk60u5t0Zf2UQhGlJWDM78Ep70XpNufIfc+f4VOTeybYKXu9pDjz87FkRzLiVsGpQG2SzuGlhaJw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.4", - "@inquirer/figures": "^1.0.9", - "@inquirer/type": "^3.0.2", + "@inquirer/core": "^10.1.5", + "@inquirer/figures": "^1.0.10", + "@inquirer/type": "^3.0.3", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, @@ -4091,9 +4091,9 @@ } }, "node_modules/@inquirer/type": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.2.tgz", - "integrity": "sha512-ZhQ4TvhwHZF+lGhQ2O/rsjo80XoZR5/5qhOY3t6FJuX5XBg5Be8YzYTvaUGJnc12AUGI2nr4QSUE4PhKSigx7g==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.3.tgz", + "integrity": "sha512-I4VIHFxUuY1bshGbXZTxCmhwaaEst9s/lll3ekok+o1Z26/ZUKdx8y1b7lsoG6rtsBDwEGfiBJ2SfirjoISLpg==", "dev": true, "license": "MIT", "engines": { @@ -5624,9 +5624,9 @@ } }, "node_modules/@ngtools/webpack": { - "version": "19.1.4", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-19.1.4.tgz", - "integrity": "sha512-ZmUlbVqu/pz8abxVxNCKgKeY5g2MX1NsKxhM8rRV5tVV/MaAtSYNHgmFSYcKWA178v7k6BUuhnoNNxl5qqc1kw==", + "version": "19.1.5", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-19.1.5.tgz", + "integrity": "sha512-oIpE5Ci/Gl2iZqa0Hs6IOxaXEDHkF/zisHcflzYGkMnYcSFj+wRgYEuBFaHLCwuxQf9OdGu31i05w849i6tY1Q==", "dev": true, "license": "MIT", "peer": true, @@ -5900,9 +5900,9 @@ } }, "node_modules/@npmcli/redact": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-3.0.0.tgz", - "integrity": "sha512-/1uFzjVcfzqrgCeGW7+SZ4hv0qLWmKXVzFahZGJ6QuJBj6Myt9s17+JL86i76NV9YSnJRcGXJYQbAU0rn1YTCQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-3.1.1.tgz", + "integrity": "sha512-3Hc2KGIkrvJWJqTbvueXzBeZlmvoOxc2jyX00yzr3+sNFquJg0N8hH4SAPLPVrkWIRQICVpVgjrss971awXVnA==", "dev": true, "license": "ISC", "engines": { @@ -6673,14 +6673,14 @@ ] }, "node_modules/@schematics/angular": { - "version": "19.1.4", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.1.4.tgz", - "integrity": "sha512-HFf83SoXbj1K4jkYSSfCg/oXkmSGBx0zG1Lh+dE5GZFdTQmykrBY519aSdrqLVyZzKYjTGfDfSewUeO4a0GE2A==", + "version": "19.1.5", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.1.5.tgz", + "integrity": "sha512-Yks2QD87z2qJhVLi6O0tQDBG4pyX5n5c8BYEyZ+yiThjzIXBRkHjWS1jIFvd/y1+yU/NQFHYG/sy8sVOxfQ9IA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.1.4", - "@angular-devkit/schematics": "19.1.4", + "@angular-devkit/core": "19.1.5", + "@angular-devkit/schematics": "19.1.5", "jsonc-parser": "3.3.1" }, "engines": { @@ -6690,63 +6690,63 @@ } }, "node_modules/@sentry-internal/browser-utils": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.51.0.tgz", - "integrity": "sha512-r94yfRK17zNJER0hgQE4qOSy5pWzsnFcGTJQSqhSEKUcC4KK37qSfoPrPejFxtIqXhqlkd/dTWKvrMwXWcn0MQ==", + "version": "8.52.1", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.52.1.tgz", + "integrity": "sha512-+GXnlJCPWxNkneojLFFdfF8rt7nQ1BIRctdZx6JneQRahC9hJ0hHR4WnIa47iB7d+3hJiJWmfe7I+k+6rMuoPA==", "license": "MIT", "dependencies": { - "@sentry/core": "8.51.0" + "@sentry/core": "8.52.1" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/feedback": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.51.0.tgz", - "integrity": "sha512-VgfxSZWLYUPKDnkt2zG+Oe5ccv8U3WPM6Mo4kfABIJT3Ai4VbZB7+vb2a4pm6lUCF9DeOPXHb5o9Tg17SHDAHw==", + "version": "8.52.1", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.52.1.tgz", + "integrity": "sha512-zakzlMHeEb+0FsPtISDNrFjiwIB/JeXc1xzelvSb9QAh3htog+snnqa5rqrRdYmAKNZM3TTe16X/aKqCJ54dCg==", "license": "MIT", "dependencies": { - "@sentry/core": "8.51.0" + "@sentry/core": "8.52.1" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/replay": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.51.0.tgz", - "integrity": "sha512-lkm7id3a2n3yMZeF5socCVQUeEeShNOGr7Wtsmb5RORacEnld0z+NfbMTilo1mDwiWBzI5OYBjm62eglm1HFsQ==", + "version": "8.52.1", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.52.1.tgz", + "integrity": "sha512-jCk+N5RknOwj3w+yECQKd0ozB3JOKLkkrpGL+v9rQxWM9mYcfcD7+WJfgQVjfqQ19NCtH3m231fTEL4BAUMFMA==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "8.51.0", - "@sentry/core": "8.51.0" + "@sentry-internal/browser-utils": "8.52.1", + "@sentry/core": "8.52.1" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/replay-canvas": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.51.0.tgz", - "integrity": "sha512-ERXIbwdULkdtIQnfkMLRVfpoGV2rClwySGRlTPepFKeLxlcXo9o09cPu+qbukiDnGK0cgEgRnrV961hMg21Bmw==", + "version": "8.52.1", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.52.1.tgz", + "integrity": "sha512-KQKRD6d3m4jTLaxGi8gASEc5kU/SxOsiQ/k1DAeTOZwRhGt63zzbBnSg6IaGZLFNqmKK+QYhoCrn3pPO7+NECg==", "license": "MIT", "dependencies": { - "@sentry-internal/replay": "8.51.0", - "@sentry/core": "8.51.0" + "@sentry-internal/replay": "8.52.1", + "@sentry/core": "8.52.1" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry/angular": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@sentry/angular/-/angular-8.51.0.tgz", - "integrity": "sha512-3hHtxmKIfBZurqTEL680gLe+5F5NpNajkg+ZmVC49/Wu9X9ubpmETBuUasqkB7q+iyx+eoE04AznzEEpLRLuOA==", + "version": "8.52.1", + "resolved": "https://registry.npmjs.org/@sentry/angular/-/angular-8.52.1.tgz", + "integrity": "sha512-Xe+x4ssAVCGG2l1MowF0hDnLnG1SL1LOt+cOHn1t3IaY+9HnCnlkW9X4asuyiFPCgxyjcVHwsjBpUK9YyWax5w==", "license": "MIT", "dependencies": { - "@sentry/browser": "8.51.0", - "@sentry/core": "8.51.0", + "@sentry/browser": "8.52.1", + "@sentry/core": "8.52.1", "tslib": "^2.4.1" }, "engines": { @@ -6760,38 +6760,38 @@ } }, "node_modules/@sentry/browser": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.51.0.tgz", - "integrity": "sha512-1kbbyVfBBAx5Xyynp+lC5lLnAHo0qJ2r4mtmdT6koPjesvoOocEK0QQnouQBmdUbm3L0L/bPI1SgXjbeJyhzHQ==", + "version": "8.52.1", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.52.1.tgz", + "integrity": "sha512-MB7NZ5zSkA5kFEGvEa/y+0pt5UFB8pToFGC2wBR0nfQfhQ9amIdv+LYPyJFGXGIIEVCIQMEnSlm1nGH4RKzZfw==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "8.51.0", - "@sentry-internal/feedback": "8.51.0", - "@sentry-internal/replay": "8.51.0", - "@sentry-internal/replay-canvas": "8.51.0", - "@sentry/core": "8.51.0" + "@sentry-internal/browser-utils": "8.52.1", + "@sentry-internal/feedback": "8.52.1", + "@sentry-internal/replay": "8.52.1", + "@sentry-internal/replay-canvas": "8.52.1", + "@sentry/core": "8.52.1" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry/core": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.51.0.tgz", - "integrity": "sha512-Go0KxCYLw+OBIlLSv5YsYX+x9NW43fNVcyB6rhkSp2Q5Zme3tAE6KtZFvyu4SO7G/903wisW5Q6qV6UuK/ee4A==", + "version": "8.52.1", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.52.1.tgz", + "integrity": "sha512-FG0P9I03xk4jBI4O7NBkw8uqLGH9/RWOSFoRH3eYvUTyBLhkk9IaCFbAAGBNZhojky8T7gqYwnuRbFNlrAiuSA==", "license": "MIT", "engines": { "node": ">=14.18" } }, "node_modules/@sentry/types": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.51.0.tgz", - "integrity": "sha512-LNwI3IrZR0OaB3u4e8PwjRCO/NZy0m3Hld8j44WnbA/fwq0V5b9PH0wG6NCISOsIBSDzun0HpHCUi3VeQoupvw==", + "version": "8.52.1", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.52.1.tgz", + "integrity": "sha512-Q8wXwEkNHNr6ahbspn85cFh/ZWTfFgp5kNQO9YngqcCgWJFVMPz3h0jhpUitH9SOqHl7kPdH9CXQDwbxiMOCJQ==", "dev": true, "license": "MIT", "dependencies": { - "@sentry/core": "8.51.0" + "@sentry/core": "8.52.1" }, "engines": { "node": ">=14.18" @@ -7305,9 +7305,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.5.tgz", - "integrity": "sha512-GLZPrd9ckqEBFMcVM/qRFAP0Hg3qiVEojgEFsx/N/zKXsBzbGF6z5FBDpZ0+Xhp1xr+qRZYjfGr1cWHB9oFHSA==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", "dev": true, "license": "MIT", "peer": true, @@ -7471,9 +7471,9 @@ "license": "MIT" }, "node_modules/@types/lodash": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.14.tgz", - "integrity": "sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A==", + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==", "dev": true, "license": "MIT" }, @@ -7514,9 +7514,9 @@ "peer": true }, "node_modules/@types/node": { - "version": "22.10.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.10.tgz", - "integrity": "sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww==", + "version": "22.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.12.0.tgz", + "integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==", "dev": true, "license": "MIT", "dependencies": { @@ -7551,12 +7551,6 @@ "@types/node": "*" } }, - "node_modules/@types/prop-types": { - "version": "15.7.14", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", - "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", - "license": "MIT" - }, "node_modules/@types/qs": { "version": "6.9.18", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", @@ -7574,12 +7568,11 @@ "peer": true }, "node_modules/@types/react": { - "version": "18.3.18", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", - "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", + "version": "19.0.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz", + "integrity": "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==", "license": "MIT", "dependencies": { - "@types/prop-types": "*", "csstype": "^3.0.2" } }, @@ -9452,9 +9445,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001695", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001695.tgz", - "integrity": "sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==", + "version": "1.0.30001696", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001696.tgz", + "integrity": "sha512-pDCPkvzfa39ehJtJ+OwGT/2yvT2SbjfHhiIW2LWOAcMQ7BzwxT/XuyUp4OTOd0XFWA6BKw0JalnBHgSi5DGJBQ==", "funding": [ { "type": "opencollective", @@ -9585,9 +9578,9 @@ } }, "node_modules/cjs-module-lexer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", - "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", "dev": true, "license": "MIT" }, @@ -10902,9 +10895,9 @@ } }, "node_modules/dompurify": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.3.tgz", - "integrity": "sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz", + "integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -10972,9 +10965,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.88", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.88.tgz", - "integrity": "sha512-K3C2qf1o+bGzbilTDCTBhTQcMS9KW60yTAaTeeXsfvQuTDDwlokLam/AdqlqcSy9u4UainDgsHV23ksXAOgamw==", + "version": "1.5.90", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.90.tgz", + "integrity": "sha512-C3PN4aydfW91Natdyd449Kw+BzhLmof6tzy5W1pFC5SpQxVXT+oyiyOG9AgYYSN9OdA/ik3YkCrpwqI8ug5Tug==", "license": "ISC" }, "node_modules/emittery": { @@ -12094,9 +12087,9 @@ "license": "BSD-3-Clause" }, "node_modules/fastq": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", - "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", + "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -15892,9 +15885,9 @@ } }, "node_modules/loupe": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", - "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", "dev": true, "license": "MIT" }, @@ -17353,9 +17346,9 @@ "license": "(MIT AND Zlib)" }, "node_modules/papaparse": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.1.tgz", - "integrity": "sha512-EuEKUhyxrHVozD7g3/ztsJn6qaKse8RPfR6buNB2dMJvdtXNhcw8jccVi/LxNEY3HVrV6GO6Z4OoeCG9Iy9wpA==", + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.2.tgz", + "integrity": "sha512-PZXg8UuAc4PcVwLosEEDYjPyfWnTEhOrUfdv+3Bx+NuAb+5NhDmXzg5fHWmdCh1mP5p7JAZfFr3IMQfcntNAdA==", "license": "MIT" }, "node_modules/parent-module": { diff --git a/package.json b/package.json index 539be2fabcfc..77ab43e5c826 100644 --- a/package.json +++ b/package.json @@ -13,18 +13,18 @@ "node_modules" ], "dependencies": { - "@angular/animations": "19.1.3", - "@angular/cdk": "19.1.1", - "@angular/common": "19.1.3", - "@angular/compiler": "19.1.3", - "@angular/core": "19.1.3", - "@angular/forms": "19.1.3", - "@angular/localize": "19.1.3", - "@angular/material": "19.1.1", - "@angular/platform-browser": "19.1.3", - "@angular/platform-browser-dynamic": "19.1.3", - "@angular/router": "19.1.3", - "@angular/service-worker": "19.1.3", + "@angular/animations": "19.1.4", + "@angular/cdk": "19.1.2", + "@angular/common": "19.1.4", + "@angular/compiler": "19.1.4", + "@angular/core": "19.1.4", + "@angular/forms": "19.1.4", + "@angular/localize": "19.1.4", + "@angular/material": "19.1.2", + "@angular/platform-browser": "19.1.4", + "@angular/platform-browser-dynamic": "19.1.4", + "@angular/router": "19.1.4", + "@angular/service-worker": "19.1.4", "@ctrl/ngx-emoji-mart": "9.2.0", "@danielmoncada/angular-datetime-picker": "19.1.0", "@fingerprintjs/fingerprintjs": "4.5.1", @@ -36,7 +36,7 @@ "@ng-bootstrap/ng-bootstrap": "18.0.0", "@ngx-translate/core": "16.0.4", "@ngx-translate/http-loader": "16.0.1", - "@sentry/angular": "8.51.0", + "@sentry/angular": "8.52.1", "@siemens/ngx-datatable": "22.4.1", "@swimlane/ngx-charts": "21.1.3", "@swimlane/ngx-graph": "9.0.1", @@ -48,7 +48,7 @@ "crypto-js": "4.2.0", "dayjs": "1.11.13", "diff-match-patch-typescript": "1.1.0", - "dompurify": "3.2.3", + "dompurify": "3.2.4", "emoji-js": "3.8.1", "export-to-csv": "1.4.0", "fast-json-patch": "3.1.1", @@ -68,7 +68,7 @@ "ngx-infinite-scroll": "19.0.0", "ngx-webstorage": "19.0.1", "pako": "2.1.0", - "papaparse": "5.5.1", + "papaparse": "5.5.2", "pdf-lib": "1.17.1", "pdfjs-dist": "4.10.38", "rxjs": "7.8.1", @@ -138,11 +138,11 @@ "@angular-eslint/eslint-plugin-template": "19.0.2", "@angular-eslint/schematics": "19.0.2", "@angular-eslint/template-parser": "19.0.2", - "@angular/build": "19.1.4", - "@angular/cli": "19.1.4", - "@angular/compiler-cli": "19.1.3", - "@angular/language-service": "19.1.3", - "@sentry/types": "8.51.0", + "@angular/build": "19.1.5", + "@angular/cli": "19.1.5", + "@angular/compiler-cli": "19.1.4", + "@angular/language-service": "19.1.4", + "@sentry/types": "8.52.1", "@testing-library/angular": "17.3.5", "@types/crypto-js": "4.2.2", "@types/d3-shape": "3.1.7", @@ -151,7 +151,7 @@ "@types/jest": "29.5.14", "@types/lodash-es": "4.17.12", "@types/markdown-it": "14.1.2", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/pako": "2.0.3", "@types/papaparse": "5.3.15", "@types/smoothscroll-polyfill": "0.3.4",