diff --git a/.github/workflows/check-translation-keys.yml b/.github/workflows/check-translation-keys.yml index a14ed3f43b9a..02dbe17f2955 100644 --- a/.github/workflows/check-translation-keys.yml +++ b/.github/workflows/check-translation-keys.yml @@ -18,7 +18,4 @@ jobs: with: python-version: "3.12" - name: Check if translation keys match - run: > - python .ci/translation-file-checker/translation_file_checker.py - --german-files src/main/webapp/i18n/de/ - --english-files src/main/webapp/i18n/en/ + run: python .ci/translation-file-checker/translation_file_checker.py --german-files src/main/webapp/i18n/de/ --english-files src/main/webapp/i18n/en/ diff --git a/.github/workflows/validate-pr-title.yml b/.github/workflows/validate-pr-title.yml new file mode 100644 index 000000000000..37e280ee2017 --- /dev/null +++ b/.github/workflows/validate-pr-title.yml @@ -0,0 +1,14 @@ +name: Validate PR Title + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review, edited] + +jobs: + validate-pr-title: + runs-on: ubuntu-latest + timeout-minutes: 1 + steps: + - uses: Slashgear/action-check-pr-title@v4.3.0 + with: + regexp: '^`(Programming exercises|Integrated code lifecycle|Quiz exercises|Modeling exercises|Text exercises|File upload exercises|Exam mode|Grading|Assessment|Communication|Notifications|Team exercises|Lectures|Integrated markdown editor|Plagiarism checks|Learning analytics|Adaptive learning|Learning path|Tutorial groups|Iris|Scalability|Usability|Performance|Infrastructure|Mobile apps|Development|General)`:\s[A-Z].*$' \ No newline at end of file diff --git a/.idea/runConfigurations/_template__of_Gradle.xml b/.idea/runConfigurations/_template__of_Gradle.xml index 0d7f0a89449e..7c6cecee1dad 100644 --- a/.idea/runConfigurations/_template__of_Gradle.xml +++ b/.idea/runConfigurations/_template__of_Gradle.xml @@ -4,7 +4,7 @@ diff --git a/README.md b/README.md index 9b6118e0fe28..08742f5d89b5 100644 --- a/README.md +++ b/README.md @@ -193,7 +193,7 @@ Refer to [Using JHipster in production](http://www.jhipster.tech/production) for The following command can automate the deployment to a server. The example shows the deployment to the main Artemis test server (which runs a virtual machine): ```shell -./artemis-server-cli deploy username@artemistest.ase.in.tum.de -w build/libs/Artemis-7.5.6.war +./artemis-server-cli deploy username@artemistest.ase.in.tum.de -w build/libs/Artemis-7.6.0.war ``` ## Architecture diff --git a/angular.json b/angular.json index e5543ff2ce60..008ac75d13bf 100644 --- a/angular.json +++ b/angular.json @@ -113,7 +113,7 @@ }, { "glob": "**/*", - "input": "./node_modules/monaco-editor/min/vs", + "input": "./node_modules/monaco-editor/bundles/vs", "output": "vs" } ], diff --git a/build.gradle b/build.gradle index 1375d3298583..163b2517748f 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ plugins { } group = "de.tum.cit.aet.artemis" -version = "7.5.6" +version = "7.6.0" description = "Interactive Learning with Individual Feedback" java { @@ -246,12 +246,15 @@ dependencies { implementation "org.gitlab4j:gitlab4j-api:6.0.0-rc.5" implementation "de.jplag:jplag:${jplag_version}" - implementation "de.jplag:java:${jplag_version}" - implementation "de.jplag:kotlin:${jplag_version}" + implementation "de.jplag:c:${jplag_version}" - implementation "de.jplag:swift:${jplag_version}" implementation "de.jplag:java:${jplag_version}" + implementation "de.jplag:javascript:${jplag_version}" + implementation "de.jplag:kotlin:${jplag_version}" implementation "de.jplag:python-3:${jplag_version}" + implementation "de.jplag:rlang:${jplag_version}" + implementation "de.jplag:rust:${jplag_version}" + implementation "de.jplag:swift:${jplag_version}" implementation "de.jplag:text:${jplag_version}" // those are transitive dependencies of JPlag Text --> Stanford NLP @@ -270,7 +273,7 @@ dependencies { } } - implementation "org.apache.logging.log4j:log4j-to-slf4j:2.24.0" + implementation "org.apache.logging.log4j:log4j-to-slf4j:2.24.1" // Note: spring-security-lti13 does not work with jakarta yet, so we built our own custom version and declare its transitive dependencies below // implementation "uk.ac.ox.ctl:spring-security-lti13:0.1.11" @@ -342,7 +345,7 @@ dependencies { implementation "tech.jhipster:jhipster-framework:${jhipster_dependencies_version}" implementation "org.springframework.boot:spring-boot-starter-cache:${spring_boot_version}" - implementation "io.micrometer:micrometer-registry-prometheus:1.13.4" + implementation "io.micrometer:micrometer-registry-prometheus:1.13.5" implementation "net.logstash.logback:logstash-logback-encoder:8.0" // Defines low-level streaming API, and includes JSON-specific implementations @@ -405,7 +408,7 @@ dependencies { implementation "org.springframework.cloud:spring-cloud-starter-config:4.1.3" implementation "org.springframework.cloud:spring-cloud-commons:4.1.4" - implementation "io.netty:netty-all:4.1.113.Final" + implementation "io.netty:netty-all:4.1.114.Final" implementation "io.projectreactor.netty:reactor-netty:1.1.22" implementation "org.springframework:spring-messaging:6.1.13" implementation "org.springframework.retry:spring-retry:2.0.9" @@ -416,7 +419,7 @@ dependencies { implementation "org.springframework.security:spring-security-oauth2-core:${spring_security_version}" implementation "org.springframework.security:spring-security-oauth2-client:${spring_security_version}" // use newest version of nimbus-jose-jwt to avoid security issues through outdated dependencies - implementation "com.nimbusds:nimbus-jose-jwt:9.41.1" + implementation "com.nimbusds:nimbus-jose-jwt:9.41.2" implementation "org.springframework.security:spring-security-oauth2-jose:${spring_security_version}" implementation "org.springframework.security:spring-security-crypto:${spring_security_version}" @@ -464,7 +467,7 @@ dependencies { implementation "com.google.code.gson:gson:2.11.0" - implementation "com.google.errorprone:error_prone_annotations:2.32.0" + implementation "com.google.errorprone:error_prone_annotations:2.33.0" // NOTE: we want to keep the same unique version for all configurations, implementation and annotationProcessor implementation("net.bytebuddy:byte-buddy") { @@ -528,11 +531,11 @@ dependencies { testImplementation "org.mockito:mockito-core:${mockito_version}" testImplementation "org.mockito:mockito-junit-jupiter:${mockito_version}" - testImplementation "io.github.classgraph:classgraph:4.8.176" + testImplementation "io.github.classgraph:classgraph:4.8.177" testImplementation "org.awaitility:awaitility:4.2.2" testImplementation "org.apache.maven.shared:maven-invoker:3.3.0" testImplementation "org.gradle:gradle-tooling-api:8.10.2" - testImplementation "org.apache.maven.surefire:surefire-report-parser:3.5.0" + testImplementation "org.apache.maven.surefire:surefire-report-parser:3.5.1" testImplementation "com.opencsv:opencsv:5.9" testImplementation("io.zonky.test:embedded-database-spring-test:2.5.1") { exclude group: "org.testcontainers", module: "mariadb" diff --git a/docs/dev/guidelines/database.rst b/docs/dev/guidelines/database.rst index 65365549f10a..ea4e436b53d4 100644 --- a/docs/dev/guidelines/database.rst +++ b/docs/dev/guidelines/database.rst @@ -295,7 +295,7 @@ Best Practices // IrisSubSettings.java @Column(name = "allowed_models") @Convert(converter = IrisModelListConverter.class) - private TreeSet allowedModels = new TreeSet<>(); + private TreeSet allowedVariants = new TreeSet<>(); * **Ordered Collection with duplicates**: When you want to order the collection of (potentially duplicated) objects of the relationship, then always use a ``List``. It is important to note here that there is no inherent order in a database table. One could argue that you can use the ``id`` field for the ordering, but there are edge cases where this can lead to problems. Therefore, for an ordered collection with duplicates, **always** annotate it with ``@OrderColumn``. An order column indicates to Hibernate that we want to order our collection based on a specific column of our data table. By default, the column name it expects is *tablenameS\_order*. For ordered collections, we also recommend that you annotate them with ``cascade = CascadeType.ALL`` and ``orphanRemoval = true``. E.g.: diff --git a/docs/index.rst b/docs/index.rst index 32c130434e4f..f4a7bc4cf449 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -37,6 +37,7 @@ All these exercises are supposed to be run either live in the lecture with insta user/grading user/courses/customizable user/markdown-support + user/integrated-code-lifecycle user/exports user/mobile-applications user/lti diff --git a/docs/user/exercises/programming-exercise-features.inc b/docs/user/exercises/programming-exercise-features.inc index 6d0b257dd96e..660e2bd4bf02 100644 --- a/docs/user/exercises/programming-exercise-features.inc +++ b/docs/user/exercises/programming-exercise-features.inc @@ -37,6 +37,8 @@ Instructors can still use those templates to generate programming exercises and +----------------------+----------+---------+ | JavaScript | yes | yes | +----------------------+----------+---------+ + | R | 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. @@ -67,9 +69,11 @@ Instructors can still use those templates to generate programming exercises and +----------------------+----------------------+----------------------+---------------------+--------------+------------------------------------------+------------------------------+----------------------------+------------------------+ | OCaml | no | no | no | no | n/a | yes | no | L: yes, J: no | +----------------------+----------------------+----------------------+---------------------+--------------+------------------------------------------+------------------------------+----------------------------+------------------------+ - | Rust | no | no | no | no | n/a | no | no | L: yes, J: no | + | Rust | no | no | yes | no | n/a | no | no | L: yes, J: no | + +----------------------+----------------------+----------------------+---------------------+--------------+------------------------------------------+------------------------------+----------------------------+------------------------+ + | JavaScript | no | no | yes | no | n/a | no | no | L: yes, J: no | +----------------------+----------------------+----------------------+---------------------+--------------+------------------------------------------+------------------------------+----------------------------+------------------------+ - | JavaScript | no | no | no | no | n/a | no | no | L: yes, J: no | + | R | no | no | yes | no | n/a | no | 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. diff --git a/docs/user/exercises/programming-repository-access.inc b/docs/user/exercises/programming-repository-access.inc index 45323fced7dd..c4e201d0f4b6 100644 --- a/docs/user/exercises/programming-repository-access.inc +++ b/docs/user/exercises/programming-repository-access.inc @@ -1,3 +1,5 @@ +If you are a student, and want to know how to use the Artemis Version Control for checking out repositories locally, :ref:`checkout this guide.` + The following tables lists the different types of repositories and gives an overview of the access rights different users have. To gain these access rights, a user must assume the indicated role in the course the repository belongs to. diff --git a/docs/user/exercises/programming.rst b/docs/user/exercises/programming.rst index 6f8a8e00782c..61f1731c4dff 100644 --- a/docs/user/exercises/programming.rst +++ b/docs/user/exercises/programming.rst @@ -224,19 +224,5 @@ Each one represents a single test case feedback and should have the format: Integrated Code Lifecycle ------------------------- -The following sections describe programming exercise features that are part of the integrated code lifecycle system. - -Repository View -^^^^^^^^^^^^^^^ - -.. include:: programming-integrated-code-lifecycle-repository-view.inc - -Build Agent View -^^^^^^^^^^^^^^^^ - -.. include:: programming-integrated-code-lifecycle-build-agent-view.inc - -Build Overview View -^^^^^^^^^^^^^^^^^^^ - -.. include:: programming-integrated-code-lifecycle-build-queue-view.inc +The Artemis Integrated Code Lifecycle system allows you to use programming exercises fully integrated within Artemis, without the need of any external tools. +Find more information on it :ref:`here`. diff --git a/docs/user/exercises/programming/build-agent-details.png b/docs/user/exercises/programming/build-agent-details.png deleted file mode 100644 index ae9d8f2561f4..000000000000 Binary files a/docs/user/exercises/programming/build-agent-details.png and /dev/null differ diff --git a/docs/user/exercises/programming/build-agent-summary.png b/docs/user/exercises/programming/build-agent-summary.png deleted file mode 100644 index 414451407f71..000000000000 Binary files a/docs/user/exercises/programming/build-agent-summary.png and /dev/null differ diff --git a/docs/user/exercises/programming/buildQueueCourseManagement.png b/docs/user/exercises/programming/buildQueueCourseManagement.png deleted file mode 100644 index dc16f6dd6a32..000000000000 Binary files a/docs/user/exercises/programming/buildQueueCourseManagement.png and /dev/null differ diff --git a/docs/user/exercises/programming/buildQueueSystemAdministration.png b/docs/user/exercises/programming/buildQueueSystemAdministration.png deleted file mode 100644 index 1a18244e443a..000000000000 Binary files a/docs/user/exercises/programming/buildQueueSystemAdministration.png and /dev/null differ diff --git a/docs/user/exercises/programming/commit-diff-view.png b/docs/user/exercises/programming/commit-diff-view.png deleted file mode 100644 index cfd5cda349cc..000000000000 Binary files a/docs/user/exercises/programming/commit-diff-view.png and /dev/null differ diff --git a/docs/user/exercises/programming/commit-history-view.png b/docs/user/exercises/programming/commit-history-view.png deleted file mode 100644 index ac691c9c3a60..000000000000 Binary files a/docs/user/exercises/programming/commit-history-view.png and /dev/null differ diff --git a/docs/user/exercises/programming/course-management-repositories.png b/docs/user/exercises/programming/course-management-repositories.png deleted file mode 100644 index 47e842da25a5..000000000000 Binary files a/docs/user/exercises/programming/course-management-repositories.png and /dev/null differ diff --git a/docs/user/exercises/programming/current-repository-content-view.png b/docs/user/exercises/programming/current-repository-content-view.png deleted file mode 100644 index 041b6411f6a1..000000000000 Binary files a/docs/user/exercises/programming/current-repository-content-view.png and /dev/null differ diff --git a/docs/user/exercises/programming/finished-build-jobs.png b/docs/user/exercises/programming/finished-build-jobs.png deleted file mode 100644 index 149a7f98819b..000000000000 Binary files a/docs/user/exercises/programming/finished-build-jobs.png and /dev/null differ diff --git a/docs/user/exercises/programming/open-repository-button.png b/docs/user/exercises/programming/open-repository-button.png deleted file mode 100644 index a758454dead7..000000000000 Binary files a/docs/user/exercises/programming/open-repository-button.png and /dev/null differ diff --git a/docs/user/exercises/programming/open-repository-instructor-participations.png b/docs/user/exercises/programming/open-repository-instructor-participations.png deleted file mode 100644 index 03e9e66e3910..000000000000 Binary files a/docs/user/exercises/programming/open-repository-instructor-participations.png and /dev/null differ diff --git a/docs/user/exercises/programming/open-repository-student.png b/docs/user/exercises/programming/open-repository-student.png deleted file mode 100644 index 5685e134d177..000000000000 Binary files a/docs/user/exercises/programming/open-repository-student.png and /dev/null differ diff --git a/docs/user/exercises/programming/queued-build-jobs.png b/docs/user/exercises/programming/queued-build-jobs.png deleted file mode 100644 index eb7165c01436..000000000000 Binary files a/docs/user/exercises/programming/queued-build-jobs.png and /dev/null differ diff --git a/docs/user/exercises/programming/running-build-jobs.png b/docs/user/exercises/programming/running-build-jobs.png deleted file mode 100644 index 08ccfa8856fd..000000000000 Binary files a/docs/user/exercises/programming/running-build-jobs.png and /dev/null differ diff --git a/docs/user/icl/general.rst b/docs/user/icl/general.rst new file mode 100644 index 000000000000..bf0132e6d20e --- /dev/null +++ b/docs/user/icl/general.rst @@ -0,0 +1,12 @@ +.. _icl-general-information: + +General Information +=================== + +Artemis offers the Integrated Code Lifecycle (ICL), a comprehensive feature designed to streamline the development process for programming exercises. +ICL combines version control, secure communication, and continuous integration to provide a seamless experience for students and instructors. +Understanding these components is crucial for effectively using Artemis, especially if you're new to concepts like SSH and Git. The key components of ICL are: + +- :ref:`Local Version Control`: Use the Local Version Control to interact with the repositories of programming exercises. +- :ref:`SSH`: Use SSH to perform Git operations on repositories. +- :ref:`Local Continuous Integration`: Exercise submissions are built and tested by the Local CI system. diff --git a/docs/user/exercises/programming-integrated-code-lifecycle-build-agent-view.inc b/docs/user/icl/local-ci-build-agent-view.inc similarity index 95% rename from docs/user/exercises/programming-integrated-code-lifecycle-build-agent-view.inc rename to docs/user/icl/local-ci-build-agent-view.inc index a59068be8589..b211e879b252 100644 --- a/docs/user/exercises/programming-integrated-code-lifecycle-build-agent-view.inc +++ b/docs/user/icl/local-ci-build-agent-view.inc @@ -5,7 +5,7 @@ The build agent view consists of two parts: the *Build Agent Summary View* and t The **Build Agent Summary View** shows a list of all build agents in the system, along with their status (idle and running), maximum number of concurrent builds, and running builds. The running builds can be canceled individually by clicking the red cancellation button next to the build. An Administrator can also cancel all running builds on a build agent by clicking the '*Cancel All*' button. -.. figure:: programming/build-agent-summary.png +.. figure:: local-ci/build-agent-summary.png :alt: Build Agent Summary View :align: center @@ -20,7 +20,7 @@ the submission time, start time, end time, the duration of the build job, the co The administrator can click on the participation ID to navigate to the participation's submission page. The administrator can also click on the commit hash to navigate to the commits details page and the course ID to navigate to the course management page. -.. figure:: programming/build-agent-details.png +.. figure:: local-ci/build-agent-details.png :alt: Build Agent Details :align: center diff --git a/docs/user/exercises/programming-integrated-code-lifecycle-build-queue-view.inc b/docs/user/icl/local-ci-build-queue-view.inc similarity index 90% rename from docs/user/exercises/programming-integrated-code-lifecycle-build-queue-view.inc rename to docs/user/icl/local-ci-build-queue-view.inc index 2c2470d9561a..7c865758f98c 100644 --- a/docs/user/exercises/programming-integrated-code-lifecycle-build-queue-view.inc +++ b/docs/user/icl/local-ci-build-queue-view.inc @@ -1,7 +1,7 @@ Artemis provides a build overview view that displays all queued, running, and finished build jobs. Access to the system-wide build overview is exclusively available to **Administrators** through the *System Administration* menu. -.. figure:: programming/buildQueueSystemAdministration.png +.. figure:: local-ci/buildQueueSystemAdministration.png :alt: Build Overview System Administration :align: center @@ -10,7 +10,7 @@ exclusively available to **Administrators** through the *System Administration* **Instructors** can access the build overview for a specific course through the *Build Overview* button located in *Course Management*. This view displays only the queued, running, and finished build jobs associated with the selected course. -.. figure:: programming/buildQueueCourseManagement.png +.. figure:: local-ci/buildQueueCourseManagement.png :alt: Build Overview Course Management :align: center @@ -27,7 +27,7 @@ Jobs are dynamically added to and removed from the queue in real-time. Users have the ability to cancel any job in progress. The table provides the following information: -.. figure:: programming/running-build-jobs.png +.. figure:: local-ci/running-build-jobs.png :alt: Running Build Jobs :align: center @@ -43,7 +43,7 @@ Users also have the option to cancel any job that is queued. The table below displays the following information: -.. figure:: programming/queued-build-jobs.png +.. figure:: local-ci/queued-build-jobs.png :alt: Queued Build Jobs :align: center @@ -59,7 +59,7 @@ Instructors can also access build logs with detailed information about the build The table provides the following information: -.. figure:: programming/finished-build-jobs.png +.. figure:: local-ci/finished-build-jobs.png :alt: Finished Build Jobs :align: center diff --git a/docs/user/icl/local-ci/build-agent-details.png b/docs/user/icl/local-ci/build-agent-details.png new file mode 100644 index 000000000000..4b5b8474781a Binary files /dev/null and b/docs/user/icl/local-ci/build-agent-details.png differ diff --git a/docs/user/icl/local-ci/build-agent-summary.png b/docs/user/icl/local-ci/build-agent-summary.png new file mode 100644 index 000000000000..ec11e28a0d1d Binary files /dev/null and b/docs/user/icl/local-ci/build-agent-summary.png differ diff --git a/docs/user/icl/local-ci/buildQueueCourseManagement.png b/docs/user/icl/local-ci/buildQueueCourseManagement.png new file mode 100644 index 000000000000..d4e255f39988 Binary files /dev/null and b/docs/user/icl/local-ci/buildQueueCourseManagement.png differ diff --git a/docs/user/icl/local-ci/buildQueueSystemAdministration.png b/docs/user/icl/local-ci/buildQueueSystemAdministration.png new file mode 100644 index 000000000000..9a857a317ce8 Binary files /dev/null and b/docs/user/icl/local-ci/buildQueueSystemAdministration.png differ diff --git a/docs/user/icl/local-ci/finished-build-jobs.png b/docs/user/icl/local-ci/finished-build-jobs.png new file mode 100644 index 000000000000..eb7b3dda190d Binary files /dev/null and b/docs/user/icl/local-ci/finished-build-jobs.png differ diff --git a/docs/user/icl/local-ci/queued-build-jobs.png b/docs/user/icl/local-ci/queued-build-jobs.png new file mode 100644 index 000000000000..acd382dfe1f8 Binary files /dev/null and b/docs/user/icl/local-ci/queued-build-jobs.png differ diff --git a/docs/user/icl/local-ci/running-build-jobs.png b/docs/user/icl/local-ci/running-build-jobs.png new file mode 100644 index 000000000000..8cc66a2e5c63 Binary files /dev/null and b/docs/user/icl/local-ci/running-build-jobs.png differ diff --git a/docs/user/icl/local-continuous-integration.rst b/docs/user/icl/local-continuous-integration.rst new file mode 100644 index 000000000000..6c3cdd9b8e65 --- /dev/null +++ b/docs/user/icl/local-continuous-integration.rst @@ -0,0 +1,27 @@ +.. _local-ci: + +Continuous Integration +====================== + +Continuous Integration (CI) is a software development practice where developers frequently merge their code changes into a shared repository. Each change is automatically built and tested, which helps to: + +- Detect and address integration issues early +- Ensure code quality and consistency +- Streamline the development process + +Artemis Local CI is our implementation of these CI principles, tailored to support the Artemis learning platform. +It provides tools and views to help users build and test in programming exercises effectively. + +This document will introduce you to the key components of Artemis Local CI: the Build Agent View and the Build Overview View. +These tools will assist you throughout your development workflow on the Artemis platform. + +Build Agent View +^^^^^^^^^^^^^^^^ + +.. include:: local-ci-build-agent-view.inc + + +Build Overview View +^^^^^^^^^^^^^^^^^^^ + +.. include:: local-ci-build-queue-view.inc diff --git a/docs/user/icl/local-vc-authentication.inc b/docs/user/icl/local-vc-authentication.inc new file mode 100644 index 000000000000..6735cd2743df --- /dev/null +++ b/docs/user/icl/local-vc-authentication.inc @@ -0,0 +1,67 @@ +Cloning a repository +^^^^^^^^^^^^^^^^^^^^ + +You can use Sourcetree, git from the terminal, or any client you like to clone your Git repository. +These instructions show you how to clone your repository using Git from the terminal. + +From the exercise view, click the code button to display the Clone dialog. +Copy the clone URL (either the SSH format or the HTTPS, with or without token). +If you are using the SSH protocol, ensure your public key is stored in your Artemis account settings and loaded on the local system to which you are cloning. +From a terminal window, change to the local directory where you want to clone your repository. + +Paste the command you copied from Bitbucket, for example: + +Clone over HTTPS: + +.. code-block:: bash + + git clone https://username@artemis.cit.tum.de/course/documentation-tests.git + + +Clone over HTTPS with access token: + +.. code-block:: bash + + git clone https://username:accessToken@artemis.cit.tum.de/course/documentation-tests.git + + +Clone over SSH: + +.. code-block:: bash + + git clone ssh://git@artemis.cit.tum.de/course/documentation-tests.git + + +If the clone was successful, a new sub-directory appears on your local drive. +This directory has the same name as the repository that you cloned. +The clone contains the files and metadata that Git requires to maintain the changes you make to the source files. + +Choosing between HTTPS and SSH: + +- HTTPS: Easier to set up initially, works through firewalls, but requires entering credentials more frequently. +- SSH: More secure, doesn't require entering passwords for each operation once set up, but initial setup can be more complex. + +Choose HTTPS if you're new to Git or working in an environment with strict firewall rules. +Choose SSH for enhanced security and convenience in long-term development. + +HTTPS access tokens +^^^^^^^^^^^^^^^^^^^ + +Instructors can create HTTP access tokens for repository access in Artemis. +They are created in the account settings and are used in place of passwords for Git over HTTPS. +For every student's exercise, Artemis automatically generates an access token, only associated with the repository of this particular exercise. +You can use these to authenticate to the Artemis Local Version Control. + +Token Creation +"""""""""""""" + +1. Go to Profile > Settings > VCS token. +2. Create a new token + +Using SSH keys to secure Git operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Artemis provides a simple way for users to connect securely to repositories, using SSH to perform Git operations. +Next is a :ref:`small and basic introduction to SSH`, and if you already know it, :ref:`here is a guide on how to create SSH keys`. +If you already have an SSH key, :ref:`learn how to add it to your Artemis account here`. + diff --git a/docs/user/exercises/programming-integrated-code-lifecycle-repository-view.inc b/docs/user/icl/local-vc-repository-view.inc similarity index 88% rename from docs/user/exercises/programming-integrated-code-lifecycle-repository-view.inc rename to docs/user/icl/local-vc-repository-view.inc index 5273befacd08..aef46936e99a 100644 --- a/docs/user/exercises/programming-integrated-code-lifecycle-repository-view.inc +++ b/docs/user/icl/local-vc-repository-view.inc @@ -3,13 +3,13 @@ These changes are stored in a repository. A repository is a collection of files How to navigate through the repository and view the code is explained in the following sections. This guide is divided into two parts: one for students and one for instructors. It is demonstrated using course programming exercises, but the same principles apply to exams as well. -For Students -"""""""""""" +Access Repositories as a Student +"""""""""""""""""""""""""""""""" As a student, you can use the repository to view your course submissions. To access your submissions, you need to navigate to the exercise, press the '*Code*' button, and click on |open-repository-button|. -.. figure:: programming/open-repository-student.png +.. figure:: local-vc/open-repository-student.png :alt: Opening the repository in the student view :align: center @@ -22,7 +22,7 @@ To the right side of the screen, above the problem statement, you can see the re the '*Code*' button to clone the repository to your local machine and see the full commit history by pressing the '*Open Commit History*' button. Finally, you can download the repository as a ZIP file by pressing the '*Download Repository*' button. -.. figure:: programming/current-repository-content-view.png +.. figure:: local-vc/current-repository-content-view.png :alt: Current Repository Content :align: center @@ -33,7 +33,7 @@ You can see the commit message, the author of the commit, the date of the commit If you created a submission with multiple commits, you can see all of them here but only the last commit has the result of the submission. Lastly, you can also view the code of the commit by pressing on the commit hash. -.. figure:: programming/commit-history-view.png +.. figure:: local-vc/commit-history-view.png :alt: Commit History View :align: center @@ -44,20 +44,20 @@ You can see the changes in the code, the files that have been added, the files t Green color indicates the lines that have been added and red color indicates the lines that have been deleted. You can also see the commit message, the author of the commit, the date of the commit and the commit hash. -.. figure:: programming/commit-diff-view.png +.. figure:: local-vc/commit-diff-view.png :alt: Commit Diff View :align: center Commit Diff View -For Instructors -""""""""""""""" +Access Repositories as an Instructor +"""""""""""""""""""""""""""""""""""" As an instructor, you can use the repository to view the submissions of your students. For this purpose, you need to navigate to the exercise participations page. Here you can see all the students’ participations for the exercise and navigate to the repository of a student by pressing the '*Code*' button and clicking on |open-repository-button|. This will show the repository of the student just like it is shown to the student. -.. figure:: programming/open-repository-instructor-participations.png +.. figure:: local-vc/open-repository-instructor-participations.png :alt: Open Repository Button in Participations Page :align: center @@ -66,11 +66,11 @@ This will show the repository of the student just like it is shown to the studen You can also see the repositories for an exercises **solution**, **template** and **test** repositories on the exercise management page. You can navigate to these repositories by pressing the '*Code*' button and clicking |open-repository-button| as described above. -.. figure:: programming/course-management-repositories.png +.. figure:: local-vc/course-management-repositories.png :alt: Solution, Template and Test Repositories in Exercise Management Page :align: center Solution, Template and Test Repositories in Exercise Management Page -.. |open-repository-button| image:: programming/open-repository-button.png +.. |open-repository-button| image:: local-vc/open-repository-button.png :scale: 50% diff --git a/docs/user/icl/local-vc/commit-diff-view.png b/docs/user/icl/local-vc/commit-diff-view.png new file mode 100644 index 000000000000..19ebb58be3ab Binary files /dev/null and b/docs/user/icl/local-vc/commit-diff-view.png differ diff --git a/docs/user/icl/local-vc/commit-history-view.png b/docs/user/icl/local-vc/commit-history-view.png new file mode 100644 index 000000000000..2dd208d0de16 Binary files /dev/null and b/docs/user/icl/local-vc/commit-history-view.png differ diff --git a/docs/user/icl/local-vc/course-management-repositories.png b/docs/user/icl/local-vc/course-management-repositories.png new file mode 100644 index 000000000000..ab0af10e9103 Binary files /dev/null and b/docs/user/icl/local-vc/course-management-repositories.png differ diff --git a/docs/user/icl/local-vc/current-repository-content-view.png b/docs/user/icl/local-vc/current-repository-content-view.png new file mode 100644 index 000000000000..961e9f17889b Binary files /dev/null and b/docs/user/icl/local-vc/current-repository-content-view.png differ diff --git a/docs/user/icl/local-vc/open-repository-button.png b/docs/user/icl/local-vc/open-repository-button.png new file mode 100644 index 000000000000..b1b4d60873cd Binary files /dev/null and b/docs/user/icl/local-vc/open-repository-button.png differ diff --git a/docs/user/icl/local-vc/open-repository-instructor-participations.png b/docs/user/icl/local-vc/open-repository-instructor-participations.png new file mode 100644 index 000000000000..bf051e6d644d Binary files /dev/null and b/docs/user/icl/local-vc/open-repository-instructor-participations.png differ diff --git a/docs/user/icl/local-vc/open-repository-student.png b/docs/user/icl/local-vc/open-repository-student.png new file mode 100644 index 000000000000..93312d1cab27 Binary files /dev/null and b/docs/user/icl/local-vc/open-repository-student.png differ diff --git a/docs/user/icl/local-vc/open-settings.png b/docs/user/icl/local-vc/open-settings.png new file mode 100644 index 000000000000..84c13407ab74 Binary files /dev/null and b/docs/user/icl/local-vc/open-settings.png differ diff --git a/docs/user/icl/local-vc/ssh-add-public-key.png b/docs/user/icl/local-vc/ssh-add-public-key.png new file mode 100644 index 000000000000..3e4683c09b7e Binary files /dev/null and b/docs/user/icl/local-vc/ssh-add-public-key.png differ diff --git a/docs/user/icl/local-version-control.rst b/docs/user/icl/local-version-control.rst new file mode 100644 index 000000000000..1ca1c1c2c31c --- /dev/null +++ b/docs/user/icl/local-version-control.rst @@ -0,0 +1,16 @@ +.. _local-vc: + +Local Version Control +===================== + +.. contents:: Content of this document + :local: + :depth: 2 + + +Repository View +^^^^^^^^^^^^^^^ + +.. include:: local-vc-repository-view.inc + +.. include:: local-vc-authentication.inc diff --git a/docs/user/icl/ssh-add-key-to-artemis.rst b/docs/user/icl/ssh-add-key-to-artemis.rst new file mode 100644 index 000000000000..0e3425faa501 --- /dev/null +++ b/docs/user/icl/ssh-add-key-to-artemis.rst @@ -0,0 +1,68 @@ +.. _use ssh key: + +Using SSH with Artemis +^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Content of this document + :local: + :depth: 1 + +You can use SSH keys to establish a secure connection between your computer and Artemis when you are performing Git operations (pull, clone, push) from your local machine. +Personal keys are linked to your Artemis account, inheriting its permissions and operating under its unique identity. +To use your generated SSH keys with Artemis, you need to add it in the account settings. + + +Add an SSH key to your Artemis account +"""""""""""""""""""""""""""""""""""""" + +**1. Copy your public key** + +On Windows in your command prompt, change directory to your `.ssh` directory, and copy the public key file to your clipboard by running: + +.. code-block:: bash + + cd %userprofile%/.ssh + clip < id_ed25519.pub + +On macOS or Linux simply run the following in a terminal: + +.. _xclip: https://wiki.ubuntuusers.de/xclip/ + +.. code-block:: bash + + pbcopy < ~/.ssh/id_ed25519.pub + +If `pbcopy` isn't working, locate the hidden `.ssh` folder, open the file in a text editor, and copy it to your clipboard. +Note that on Linux, you may need to download and install `xclip`_, then use that, as shown in this code snippet: + +.. code-block:: bash + + sudo apt-get install xclip + xclip -sel clip < ~/.ssh/id_ed25519.pub + +Note that the key's name is not necessarily **id_ed25519.pub**, but can be arbitrary, and depends on how you saved it. + + +**2. Add the key to your Artemis account** + +Open the settings, go to the SSH tab, and select 'New Key'. +Then paste the copied SSH key into the text box. + ++---------------------------------------------------+--------------------------------------------------------------+ +|.. figure:: local-vc/open-settings.png | .. figure:: local-vc/ssh-add-public-key.png | +| :alt: Open account settings | :alt: Add public SSH key to account | +| :align: center | :align: center | +| | | +| Open you Artemis account settings | Add public SSH key to account in account settings | ++---------------------------------------------------+--------------------------------------------------------------+ + +**3. Save the key. You're done!** + +Use SSH to connect to Artemis repositories +"""""""""""""""""""""""""""""""""""""""""" + +After everything is set up, you can go to a programming exercise, and use the SSH clone URL with git to access the repository locally, like this, for example: + +.. code-block:: bash + + git clone ssh://git@artemis.cit.tum.de:7921/git/COURSE/exercise-user_1.git diff --git a/docs/user/icl/ssh-intro.rst b/docs/user/icl/ssh-intro.rst new file mode 100644 index 000000000000..fd41034ebe5d --- /dev/null +++ b/docs/user/icl/ssh-intro.rst @@ -0,0 +1,67 @@ +.. _basic SSH introduction: + +SSH +^^^ + +.. contents:: Content of this document + :local: + :depth: 2 + +Artemis uses SSH as a simple way for users to connect securely to repositories to perform Git operations. + +What is SSH? +"""""""""""" + +.. _SSH (Secure Shell): https://en.wikipedia.org/wiki/Secure_Shell + +`SSH (Secure Shell)`_ is a protocol that allows you to securely connect to another computer over a network. +It’s mostly used by system administrators, developers, and IT professionals to remotely manage servers or computers. +SSH provides a secure and encrypted communication channel between your computer and a remote machine, so any data passed (like passwords or commands) is protected from eavesdropping. +In Artemis you an use SSH to access your repositories with Git. + +Why use SSH? +"""""""""""" + +The main advantage of SSH is security. +When you connect to a remote machine using SSH, all the data exchanged between your computer and the server is encrypted. +This means if someone tries to intercept the communication, they can't read it. It's like sending messages through a locked box that only you and the server can open. + +How does SSH work? +"""""""""""""""""" + +SSH works by using two components: + +- Client: The computer you are using to connect. +- Server: The machine you want to connect to. + +When you want to connect, your SSH client sends a request to the server. +If the connection is successful, you can log in to the server and start working as if you were sitting in front of it. +The connection uses SSH keys for authentication. Although it is also possible to use username and password to connect over SSH, this is discouraged. + +What are SSH Keys? +"""""""""""""""""" + +.. _public-key cryptography: https://en.wikipedia.org/wiki/Public-key_cryptography + + +SSH keys are a more secure alternative to passwords for logging into a server. +They are based on `public-key cryptography`_ and come in pairs: a public key and a private key. + +- Public Key: This key is stored on the server. Think of it like a lock that only you can open. +- Private Key: This key stays on your local machine (never shared!). It’s like the key to that lock. + +When you try to connect to the server, your computer proves it has the private key that matches the server's public key, granting you access. +You can add a personal SSH key to your user account to easily authenticate when performing read operations from your local machine. +An Artemis user can currently add one key to their account. +For instructions on how to add your SSH key to your Artemis account, please refer to :ref:`the relevant documentation`. + +Before you can use SSH keys to secure a connection with Artemis the following must have already been done: + +- SSH is enabled on your university's Artemis instance +- You need an SSH key! See :ref:`Creating SSH keys`. + +.. note:: + + - You can use the same SSH key for multiple repositories or projects. + - An Artemis user can currently only add one key to their account. + - Artemis supports ECDSA, RSA2, and Ed25519 key types. diff --git a/docs/user/icl/ssh-key-creation.rst b/docs/user/icl/ssh-key-creation.rst new file mode 100644 index 000000000000..58e7b01f5a4d --- /dev/null +++ b/docs/user/icl/ssh-key-creation.rst @@ -0,0 +1,161 @@ +.. _create ssh key: + +Creating SSH keys +^^^^^^^^^^^^^^^^^ + +.. contents:: Content of this document + :local: + :depth: 2 + +SSH keys can be used to establish a secure connection with the Artemis Local Version Control, where you are performing Git operations from your local machine. +The SSH key needs to be added to Artemis before you can make use of the key. + +Creating an SSH key on Windows +"""""""""""""""""""""""""""""" + +**1. Check for existing keys** + +You should check for existing SSH keys on your local computer. Open a command prompt, and run: + +.. code-block:: bash + + cd %userprofile%/.ssh + +- If you see "No such file or directory", then there aren't any existing keys: go to step 3. + +- Check to see if you have a key already: + +.. code-block:: bash + + dir id_* + +If there are existing keys, you may want to use those: :ref:`Add your key to Artemis`. + +**2. Back up old SSH keys** + +If you have existing SSH keys, but you don't want to use them when connecting to Bitbucket, you should back those up. +In a command prompt on your local computer, run: + +.. code-block:: bash + + mkdir key_backup + copy * key_backup + +**3. Generate a new SSH key** + +If you don't have an existing SSH key that you wish to use, generate one as follows: +1. Log in to your local computer as an administrator. +2. In a command prompt, run: + +.. _Git (with Git Bash): https://gitforwindows.org/ + + +.. code-block:: bash + + ssh-keygen -t ed25519 -C "your_email@example.com" + +Associating the key with your email address helps you to identify the key later on. +Note that the `ssh-keygen` command is only available if you have already installed `Git (with Git Bash)`_. +You'll see a response similar to this: + +.. code-block:: bash + + C:\Users\artemis>ssh-keygen -t ed25519 -C "your_email@example.com" + Generating public/private ed25519 key pair. + Enter file in which to save the key (/c/Users/artemis/.ssh/id_ed25519): + +3. Just press to accept the default location and file name. If the .ssh directory doesn't exist, the system creates one for you. +4. Enter, and re-enter, a passphrase when prompted. The whole interaction will look similar to this: + +.. code-block:: bash + + C:\Users\artemis>ssh-keygen -t ed25519 -C "your_email@example.com" + Generating public/private ed25519 key pair. + Enter file in which to save the key (/c/Users/artemis/.ssh/id_ed25519): + Created directory '/c/Users/artemis/.ssh'. + Enter passphrase (empty for no passphrase): + Enter same passphrase again: + Your identification has been saved in c/Users/artemis/.ssh/id_ed25519. + Your public key has been saved in c/Users/artemis/.ssh/id_ed25519.pub. + The key fingerprint is: + SHA256:wvaHYeLtY6+DlvV5sFZgDi3abcdefghijklmnopqrstuvw your_email@example.com + +5. You're done and you can now :ref:`add your key to Artemis`. + +Creating an SSH key on Linux & macOS +"""""""""""""""""""""""""""""""""""" + +**1. Check for existing SSH keys** + +You should check for existing SSH keys on your local computer. Open a terminal and run: + +.. code-block:: bash + + cd ~/.ssh + +If you see "No such file or directory, then there aren't any existing keys: go to step 3. +Check to see if you have a key already: + +.. code-block:: bash + + ls id_* + +If there are existing keys, you may want to use those: :ref:`Add your key to Artemis`. + +**2. Back up old SSH keys** + +If you have existing SSH keys, but you don't want to use them when connecting to Bitbucket, you should back those up. +In a command prompt on your local computer, run: + +.. code-block:: bash + + mkdir key_backup + cp * key_backup + +**3. Generate a new SSH key** + +If you don't have an existing SSH key that you wish to use, generate one as follows: + +1. Open a terminal on your local computer and enter the following: + +.. code-block:: bash + + ssh-keygen -t ed25519 -C "your_email@example.com" + +Associating the key with your email address helps you to identify the key later on. You'll see a response similar to this: + +.. code-block:: bash + + artemis@homemac ~ % ssh-keygen -t ed25519 -C artemis@email.com + Generating public/private ed25519 key pair. + Enter file in which to save the key (/Users/artemis/.ssh/id_ed25519): + +2. Just press to accept the default location and file name. If the .ssh directory doesn't exist, the system creates one for you. +3. Enter, and re-enter, a passphrase when prompted. The whole interaction will look similar to this: + +.. code-block:: bash + + artemis@homemac ~ % ssh-keygen -t ed25519 -C artemis@email.com + Generating public/private ed25519 key pair. + Enter file in which to save the key (/Users/artemis/.ssh/id_ed25519): + Enter passphrase (empty for no passphrase): + Enter same passphrase again: + Your identification has been saved in /Users/artemis/.ssh/id_ed25519. + Your public key has been saved in /Users/artemis/.ssh/id_ed25519.pub. + The key fingerprint is: + SHA256:gTVWKbn41z6JgBNu3wYjLC4abcdefghijklmnopqrstuvwxy artemis@email.com + The keys randomart image is: + +--[ED25519 256]--+ + |==+. +o.. | + |.oE. +o.. | + | . ...o | + | .o... | + | oo+S . | + | + ..B = . . | + |.+.+.oo+ * o . | + |o++.o+ . + + | + |B+ o. . . | + +----[SHA256]-----+ + artemis@homemac ~ % + +4. You're done and you can now :ref:`add your key to Artemis`. diff --git a/docs/user/integrated-code-lifecycle.rst b/docs/user/integrated-code-lifecycle.rst new file mode 100644 index 000000000000..9e5ab5231ef0 --- /dev/null +++ b/docs/user/integrated-code-lifecycle.rst @@ -0,0 +1,15 @@ +.. _integrated code lifecycle: + +Integrated Code Lifecycle +========================= + +Artemis' Integrated Code Lifecycle consists of two main components: Local Version Control and Local Continuous Integration. + +.. toctree:: + + icl/general + icl/local-version-control + icl/ssh-intro + icl/ssh-key-creation + icl/ssh-add-key-to-artemis + icl/local-continuous-integration diff --git a/gradle.properties b/gradle.properties index fc54fad2849b..07ee79d07d25 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,29 +18,29 @@ jaxb_runtime_version=4.0.5 hazelcast_version=5.5.0 fasterxml_version=2.18.0 jgit_version=7.0.0.202409031743-r -sshd_version=2.13.2 -checkstyle_version=10.18.1 +sshd_version=2.14.0 +checkstyle_version=10.18.2 jplag_version=5.1.0 # not really used in Artemis, nor Jplag, nor the used version of Stanford CoreNLP, but we use the latest to avoid security vulnerabilities # NOTE: we do not need to use the latest version 9.x here as long as Stanford CoreNLP does not reference it lucene_version=8.11.4 slf4j_version=2.0.16 -sentry_version=7.14.0 +sentry_version=7.15.0 liquibase_version=4.29.2 docker_java_version=3.4.0 -logback_version=1.5.8 +logback_version=1.5.10 java_parser_version=3.26.2 -byte_buddy_version=1.15.3 +byte_buddy_version=1.15.4 # testing # make sure both versions are compatible junit_version=5.11.0 -junit_platform_version=1.11.1 -mockito_version=5.13.0 +junit_platform_version=1.11.2 +mockito_version=5.14.1 # gradle plugin version -gradle_node_plugin_version=7.0.2 +gradle_node_plugin_version=7.1.0 apt_plugin_version=0.21 liquibase_plugin_version=2.1.1 modernizer_plugin_version=1.9.3 diff --git a/jest.config.js b/jest.config.js index 41354957ab0d..9855e511a99a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -102,10 +102,10 @@ module.exports = { coverageThreshold: { global: { // TODO: in the future, the following values should increase to at least 90% - statements: 87.35, - branches: 73.57, - functions: 81.91, - lines: 87.41, + statements: 87.39, + branches: 73.60, + functions: 81.97, + lines: 87.45, }, }, coverageReporters: ['clover', 'json', 'lcov', 'text-summary'], diff --git a/package-lock.json b/package-lock.json index bc1d3b7dff03..91ee03d72aaf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,27 +1,27 @@ { "name": "artemis", - "version": "7.5.6", + "version": "7.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "artemis", - "version": "7.5.6", + "version": "7.6.0", "hasInstallScript": true, "license": "MIT", "dependencies": { - "@angular/animations": "18.2.6", - "@angular/cdk": "18.2.6", - "@angular/common": "18.2.6", - "@angular/compiler": "18.2.6", - "@angular/core": "18.2.6", - "@angular/forms": "18.2.6", - "@angular/localize": "18.2.6", - "@angular/material": "18.2.6", - "@angular/platform-browser": "18.2.6", - "@angular/platform-browser-dynamic": "18.2.6", - "@angular/router": "18.2.6", - "@angular/service-worker": "18.2.6", + "@angular/animations": "18.2.8", + "@angular/cdk": "18.2.8", + "@angular/common": "18.2.8", + "@angular/compiler": "18.2.8", + "@angular/core": "18.2.8", + "@angular/forms": "18.2.8", + "@angular/localize": "18.2.8", + "@angular/material": "18.2.8", + "@angular/platform-browser": "18.2.8", + "@angular/platform-browser-dynamic": "18.2.8", + "@angular/router": "18.2.8", + "@angular/service-worker": "18.2.8", "@ctrl/ngx-emoji-mart": "9.2.0", "@danielmoncada/angular-datetime-picker": "18.1.0", "@fingerprintjs/fingerprintjs": "4.5.0", @@ -33,7 +33,7 @@ "@ng-bootstrap/ng-bootstrap": "17.0.1", "@ngx-translate/core": "15.0.0", "@ngx-translate/http-loader": "8.0.0", - "@sentry/angular": "8.32.0", + "@sentry/angular": "8.34.0", "@siemens/ngx-datatable": "22.4.1", "@swimlane/ngx-charts": "20.5.0", "@swimlane/ngx-graph": "8.4.0", @@ -55,12 +55,12 @@ "jszip": "3.10.1", "lodash-es": "4.17.21", "mobile-drag-drop": "3.0.0-rc.0", - "monaco-editor": "0.51.0", + "monaco-editor": "0.52.0", "ngx-infinite-scroll": "18.0.0", "ngx-webstorage": "18.0.0", "papaparse": "5.4.1", - "pdfjs-dist": "4.6.82", - "posthog-js": "1.165.0", + "pdfjs-dist": "4.7.76", + "posthog-js": "1.167.0", "rxjs": "7.8.1", "showdown": "2.1.0", "showdown-highlight": "3.1.0", @@ -78,30 +78,30 @@ }, "devDependencies": { "@angular-builders/jest": "18.0.0", - "@angular-devkit/build-angular": "18.2.6", + "@angular-devkit/build-angular": "18.2.8", "@angular-eslint/builder": "18.3.1", "@angular-eslint/eslint-plugin": "18.3.1", "@angular-eslint/eslint-plugin-template": "18.3.1", "@angular-eslint/schematics": "18.3.1", "@angular-eslint/template-parser": "18.3.1", - "@angular/cli": "18.2.6", - "@angular/compiler-cli": "18.2.6", - "@angular/language-service": "18.2.6", - "@sentry/types": "8.32.0", + "@angular/cli": "18.2.8", + "@angular/compiler-cli": "18.2.8", + "@angular/language-service": "18.2.8", + "@sentry/types": "8.34.0", "@types/crypto-js": "4.2.2", "@types/d3-shape": "3.1.6", "@types/dompurify": "3.0.5", "@types/jest": "29.5.13", "@types/lodash-es": "4.17.12", - "@types/node": "22.7.3", + "@types/node": "22.7.5", "@types/papaparse": "5.3.14", "@types/showdown": "2.0.6", "@types/smoothscroll-polyfill": "0.3.4", "@types/sockjs-client": "1.5.4", "@types/uuid": "10.0.0", - "@typescript-eslint/eslint-plugin": "8.7.0", - "@typescript-eslint/parser": "8.7.0", - "eslint": "9.11.1", + "@typescript-eslint/eslint-plugin": "8.8.1", + "@typescript-eslint/parser": "8.8.1", + "eslint": "9.12.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-deprecation": "3.0.0", "eslint-plugin-jest": "28.8.3", @@ -113,7 +113,7 @@ "jest-canvas-mock": "2.5.2", "jest-date-mock": "1.0.10", "jest-extended": "4.0.2", - "jest-fail-on-console": "3.3.0", + "jest-fail-on-console": "3.3.1", "jest-junit": "16.0.0", "jest-preset-angular": "14.2.4", "lint-staged": "15.2.10", @@ -121,7 +121,7 @@ "ngxtension": "4.0.0", "prettier": "3.3.3", "rimraf": "6.0.1", - "sass": "1.79.3", + "sass": "1.79.5", "ts-jest": "29.2.5", "typescript": "5.5.4", "weak-napi": "2.0.2" @@ -212,13 +212,13 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1802.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.6.tgz", - "integrity": "sha512-oF7cPFdTLxeuvXkK/opSdIxZ1E4LrBbmuytQ/nCoAGOaKBWdqvwagRZ6jVhaI0Gwu48rkcV7Zhesg/ESNnROdw==", + "version": "0.1802.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.8.tgz", + "integrity": "sha512-/rtFQEKgS7LlB9oHr4NCBSdKnvP5kr8L5Hbd3Vl8hZOYK9QWjxKPEXnryA2d5+PCE98bBzZswCNXqELZCPTgIQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.6", + "@angular-devkit/core": "18.2.8", "rxjs": "7.8.1" }, "engines": { @@ -228,17 +228,17 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.6.tgz", - "integrity": "sha512-u12cJZttgs5j7gICHWSmcaTCu0EFXEzKqI8nkYCwq2MtuJlAXiMQSXYuEP9OU3Go4vMAPtQh2kShyOWCX5b4EQ==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.8.tgz", + "integrity": "sha512-qK/iLk7A8vQp1CyiJV4DpwfLjPKoiOlTtFqoO5vD8Tyxmc+R06FQp6GJTsZ7JtrTLYSiH+QAWiY6NgF/Rj/hHg==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.6", - "@angular-devkit/build-webpack": "0.1802.6", - "@angular-devkit/core": "18.2.6", - "@angular/build": "18.2.6", + "@angular-devkit/architect": "0.1802.8", + "@angular-devkit/build-webpack": "0.1802.8", + "@angular-devkit/core": "18.2.8", + "@angular/build": "18.2.8", "@babel/core": "7.25.2", "@babel/generator": "7.25.0", "@babel/helper-annotate-as-pure": "7.24.7", @@ -249,7 +249,7 @@ "@babel/preset-env": "7.25.3", "@babel/runtime": "7.25.0", "@discoveryjs/json-ext": "0.6.1", - "@ngtools/webpack": "18.2.6", + "@ngtools/webpack": "18.2.8", "@vitejs/plugin-basic-ssl": "1.1.0", "ansi-colors": "4.1.3", "autoprefixer": "10.4.20", @@ -382,13 +382,13 @@ "license": "0BSD" }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1802.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.6.tgz", - "integrity": "sha512-JMLcXFaitJplwZMKkqhbYirINCRD6eOPZuIGaIOVynXYGWgvJkLT9t5C2wm9HqSLtp1K7NcYG2Y7PtTVR4krnQ==", + "version": "0.1802.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.8.tgz", + "integrity": "sha512-uPpopkXkO66SSdjtVr7xCyQCPs/x6KUC76xkDc4j0b8EEHifTbi/fNpbkcZ6wBmoAfjKLWXfKvtkh0TqKK5Hkw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1802.6", + "@angular-devkit/architect": "0.1802.8", "rxjs": "7.8.1" }, "engines": { @@ -402,9 +402,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.6.tgz", - "integrity": "sha512-la4CFvs5PcRWSkQ/H7TB5cPZirFVA9GoWk5LzIk8si6VjWBJRm8b3keKJoC9LlNeABRUIR5z0ocYkyQQUhdMfg==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.8.tgz", + "integrity": "sha512-4o2T6wsmXGE/v53+F8L7kGoN2+qzt03C9rtjLVQpOljzpJVttQ8bhvfWxyYLWwcl04RWqRa+82fpIZtBkOlZJw==", "dev": true, "license": "MIT", "dependencies": { @@ -430,13 +430,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.6.tgz", - "integrity": "sha512-uIttrQ2cQ2PWAFFVPeCoNR8xvs7tPJ2i8gzqsIwYdge107xDC6u9CqfgmBqPDSFpWj+IiC2Jwcm8Z4HYKU4+7A==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.8.tgz", + "integrity": "sha512-i/h2Oji5FhJMC7wDSnIl5XUe/qym+C1ZwScaATJwDyRLCUIynZkj5rLgdG/uK6l+H0PgvxigkF+akWpokkwW6w==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.6", + "@angular-devkit/core": "18.2.8", "jsonc-parser": "3.3.1", "magic-string": "0.30.11", "ora": "5.4.1", @@ -549,9 +549,9 @@ } }, "node_modules/@angular/animations": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.6.tgz", - "integrity": "sha512-vy9wy+Q9beiRxkEO8wNxFQ63AqAujGvk8AUHepxxIT7QNNc512TNKz8uH+feWDPO38Dm2obwYQHMGzs3WO7pUA==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.8.tgz", + "integrity": "sha512-dMSn2hg70siv3lhP+vqhMbgc923xw6XBUvnpCPEzhZqFHvPXfh/LubmsD5RtqHmjWebXtgVcgS+zg3Gq3jB2lg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -560,18 +560,18 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.6" + "@angular/core": "18.2.8" } }, "node_modules/@angular/build": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.6.tgz", - "integrity": "sha512-TQzX6Mi7uXFvmz7+OVl4Za7WawYPcx+B5Ewm6IY/DdMyB9P/Z4tbKb1LO+ynWUXYwm7avXo6XQQ4m5ArDY5F/A==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.8.tgz", + "integrity": "sha512-ufuA4vHJSrL9SQW7bKV61DOoN1mm0t0ILTHaxSoCG3YF70cZJOX7+HNp3cK2uoldRMwbTOKSvCWBw54KKDRd5Q==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.6", + "@angular-devkit/architect": "0.1802.8", "@babel/core": "7.25.2", "@babel/helper-annotate-as-pure": "7.24.7", "@babel/helper-split-export-declaration": "7.24.7", @@ -651,9 +651,9 @@ } }, "node_modules/@angular/cdk": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.6.tgz", - "integrity": "sha512-Gfq/iv4zhlKYpdQkDaBRwxI71NHNUHM1Cs1XhnZ0/oFct5HXvSv1RHRGTKqBJLLACaAPzZKXJ/UglLoyO5CNiQ==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.8.tgz", + "integrity": "sha512-J8A2FkwTBzLleAEWz6EgW73dEoeq87GREBPjTv8+2JV09LX+V3hnbgNk6zWq5k4OXtQNg9WrWP9QyRbUyA597g==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -668,18 +668,18 @@ } }, "node_modules/@angular/cli": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.6.tgz", - "integrity": "sha512-tdXsnV/w+Rgu8q0zFsLU5L9ImTVqrTol1vppHaQkJ/vuoHy+s8ZEbBqhVrO/ffosNb2xseUybGYvqMS4zkNQjg==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.8.tgz", + "integrity": "sha512-GKXG7F7z5rxwZ8/bnW/Bp8/zsfE/BpHmIP/icLfUIOwv2kaY5OD2tfQssWXPEuqZzYq2AYz+wjVSbWjxGoja8A==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1802.6", - "@angular-devkit/core": "18.2.6", - "@angular-devkit/schematics": "18.2.6", + "@angular-devkit/architect": "0.1802.8", + "@angular-devkit/core": "18.2.8", + "@angular-devkit/schematics": "18.2.8", "@inquirer/prompts": "5.3.8", "@listr2/prompt-adapter-inquirer": "2.0.15", - "@schematics/angular": "18.2.6", + "@schematics/angular": "18.2.8", "@yarnpkg/lockfile": "1.1.0", "ini": "4.1.3", "jsonc-parser": "3.3.1", @@ -702,9 +702,9 @@ } }, "node_modules/@angular/common": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.6.tgz", - "integrity": "sha512-89793ow+wrI1c7C6kyMbnweLNIZHzXthosxAEjipRZGBrqBYjvTtkE45Fl+5yBa3JO7bAhyGkUnEoyvWtZIAEA==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.8.tgz", + "integrity": "sha512-TYsKtE5nVaIScWSLGSO34Skc+s3hB/BujSddnfQHoNFvPT/WR0dfmdlpVCTeLj+f50htFoMhW11tW99PbK+whQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -713,14 +713,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.6", + "@angular/core": "18.2.8", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.6.tgz", - "integrity": "sha512-3tX2/Qw+bZ8XzKitviH8jzNGyY0uohhehhBB57OJOCc+yr4ojy/7SYFnun1lSsRnDztdCE461641X4iQLCQ94w==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.8.tgz", + "integrity": "sha512-JRedHNfK1CCPVyeGQB5w3WBYqMA6X8Q240CkvjlGfn0pVXihf9DWk3nkSQJVgYxpvpHfxdgjaYZ5IpMzlkmkhw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -729,7 +729,7 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.6" + "@angular/core": "18.2.8" }, "peerDependenciesMeta": { "@angular/core": { @@ -738,14 +738,14 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.6.tgz", - "integrity": "sha512-b5x9STfjNiNM/S0D+CnqRP9UOxPtSz1+RlCH5WdOMiW/p8j5p6dBix8YYgTe6Wg3OD7eItD2pnFQKgF/dWiopA==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.8.tgz", + "integrity": "sha512-OksDE4LWQUCcIvMjtZF7eiDCdIMrcMMpC1+Q0PIYi7KmnqXFGs4/Y0NdJvtn/LrQznzz5WaKM3ZDVNZTRX4wmw==", "license": "MIT", "dependencies": { "@babel/core": "7.25.2", "@jridgewell/sourcemap-codec": "^1.4.14", - "chokidar": "^3.0.0", + "chokidar": "^4.0.0", "convert-source-map": "^1.5.1", "reflect-metadata": "^0.2.0", "semver": "^7.0.0", @@ -761,14 +761,42 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "18.2.6", + "@angular/compiler": "18.2.8", "typescript": ">=5.4 <5.6" } }, + "node_modules/@angular/compiler-cli/node_modules/chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@angular/compiler-cli/node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "license": "MIT", + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@angular/core": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.6.tgz", - "integrity": "sha512-PjFad2j4YBwLVTw+0Te8CJCa/tV0W8caTHG8aOjj3ObdL6ihGI+FKnwerLc9RVzDFd14BOO4C6/+LbOQAh3Ltw==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.8.tgz", + "integrity": "sha512-NwIuX/Iby1jT6Iv1/s6S3wOFf8xfuQR3MPGvKhGgNtjXLbHG+TXceK9+QPZC0s9/Z8JR/hz+li34B79GrIKgUg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -782,9 +810,9 @@ } }, "node_modules/@angular/forms": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.6.tgz", - "integrity": "sha512-quGkUqTxlBaLB8C/RnpfFG57fdmNF5RQ+368N89Ma++2lpIsVAHaGZZn4yOyo3wNYaM2jBxNqaYxOzZNUl5Tig==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.8.tgz", + "integrity": "sha512-JCLki7KC6D5vF6dE6yGlBmW33khIgpHs8N9SzuiJtkQqNDTIQA8cPsGV6qpLpxflxASynQOX5lDkWYdQyfm77Q==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -793,16 +821,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.6", - "@angular/core": "18.2.6", - "@angular/platform-browser": "18.2.6", + "@angular/common": "18.2.8", + "@angular/core": "18.2.8", + "@angular/platform-browser": "18.2.8", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/language-service": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-18.2.6.tgz", - "integrity": "sha512-GBvBvS2llh+/l2YhO7UO5o3GftlvQQoXnw3v0hcNoHKwcnvqXV4CCi+T2WOaZyK0iB8Is4QRbMrpJUC66HokZg==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-18.2.8.tgz", + "integrity": "sha512-IueQ57CPP0Dt0z2n8B1A6JTwTq6m/AJVObZzrkSfXlzY1rY2qRuTJmAbZpTJ3iAxVzNYoaGh+NFHmJL8fRiXKQ==", "dev": true, "license": "MIT", "engines": { @@ -810,9 +838,9 @@ } }, "node_modules/@angular/localize": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-18.2.6.tgz", - "integrity": "sha512-4NZwh5EAyXItmwv6hqilV+JyN8DT+d+S1rW+M1IwJqC9asCDfpFqipKpuQF81LQKeLH0mn/phNfVbnJCLP0Tkw==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-18.2.8.tgz", + "integrity": "sha512-1T7aXEdgVyeYnHOfQUuIDO8Lsamg1ZLrJrA5zUv61asPJp6HCcMjXy9vDQ1XvHm5+CdDjKk/rczlN4lSMZ0QRw==", "license": "MIT", "dependencies": { "@babel/core": "7.25.2", @@ -829,21 +857,21 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "18.2.6", - "@angular/compiler-cli": "18.2.6" + "@angular/compiler": "18.2.8", + "@angular/compiler-cli": "18.2.8" } }, "node_modules/@angular/material": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-18.2.6.tgz", - "integrity": "sha512-ObxC/vomSb9QF3vIztuiInQzws+D6u09Dhfx6uNFjtyICqxEFpF7+Qx7QVDWrsuXOgxZTKgacK8f46iV8hWUfg==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-18.2.8.tgz", + "integrity": "sha512-wQGMVsfQ9lQfih2VsWAvV4z3S3uBxrxc61owlE+K0T1BxH9u/jo3A/rnRitIdvR/L4NnYlfhCnmrW9K+Pl+WCg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/animations": "^18.0.0 || ^19.0.0", - "@angular/cdk": "18.2.6", + "@angular/cdk": "18.2.8", "@angular/common": "^18.0.0 || ^19.0.0", "@angular/core": "^18.0.0 || ^19.0.0", "@angular/forms": "^18.0.0 || ^19.0.0", @@ -852,9 +880,9 @@ } }, "node_modules/@angular/platform-browser": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.6.tgz", - "integrity": "sha512-RA8UMiYNLga+QMwpKcDw1357gYPfPyY/rmLeezMak//BbsENFYQOJ4Z6DBOBNiPlHxmBsUJMGaKdlpQhfCROyQ==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.8.tgz", + "integrity": "sha512-EPai4ZPqSq3ilLJUC85kPi9wo5j5suQovwtgRyjM/75D9Qy4TV19g8hkVM5Co/zrltO8a2G6vDscCNI5BeGw2A==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -863,9 +891,9 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/animations": "18.2.6", - "@angular/common": "18.2.6", - "@angular/core": "18.2.6" + "@angular/animations": "18.2.8", + "@angular/common": "18.2.8", + "@angular/core": "18.2.8" }, "peerDependenciesMeta": { "@angular/animations": { @@ -874,9 +902,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.6.tgz", - "integrity": "sha512-kGBU3FNc+DF9r33hwHZqiWoZgQbCDdEIucU0NCLCIg0Hw6/Q9Hr2ndjxQI+WynCPg0JeBn34jpouvpeJer3YDQ==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.8.tgz", + "integrity": "sha512-poZoapDqyN/rxGKQ3C6esdPiPLMkSpP2v12hoEa12KHgfPk7T1e+a+NMyJjV8HeOY3WyvL7tGRhW0NPTajTkhw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -885,16 +913,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.6", - "@angular/compiler": "18.2.6", - "@angular/core": "18.2.6", - "@angular/platform-browser": "18.2.6" + "@angular/common": "18.2.8", + "@angular/compiler": "18.2.8", + "@angular/core": "18.2.8", + "@angular/platform-browser": "18.2.8" } }, "node_modules/@angular/router": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.6.tgz", - "integrity": "sha512-t57Sqja8unHhZlPr+4CWnQacuox2M4p2pMHps+31wt337qH6mKf4jqDmK0dE/MFdRyKjT2a2E/2NwtxXxcWNuw==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.8.tgz", + "integrity": "sha512-L+olYgxIiBq+tbfayVI0cv1yOuymsw33msnGC2l/vpc9sSVfqGzESFnB4yMVU3vHtE9v6v2Y6O+iV44/b79W/g==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -903,16 +931,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.6", - "@angular/core": "18.2.6", - "@angular/platform-browser": "18.2.6", + "@angular/common": "18.2.8", + "@angular/core": "18.2.8", + "@angular/platform-browser": "18.2.8", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/service-worker": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-18.2.6.tgz", - "integrity": "sha512-KNqRAunG0yj3jVA/YYKH9wbAe261gAIwKeQsJyeMHGR48H88tSKdcstttNZZ3S6wdhp7tcyUC526Fc4phXnSJw==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-18.2.8.tgz", + "integrity": "sha512-LQktgS2Hn845ASWNyjde18V+CHkkPeCzORfh0ChYKiOmXYFtj/myEik5o/QI/G13Kaymy+vcuwQKiUuZjZiD1w==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -924,17 +952,17 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.6", - "@angular/core": "18.2.6" + "@angular/common": "18.2.8", + "@angular/core": "18.2.8" } }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", + "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", "license": "MIT", "dependencies": { - "@babel/highlight": "^7.24.7", + "@babel/highlight": "^7.25.7", "picocolors": "^1.0.0" }, "engines": { @@ -942,9 +970,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", - "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.8.tgz", + "integrity": "sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1014,28 +1042,28 @@ } }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz", - "integrity": "sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.25.7.tgz", + "integrity": "sha512-12xfNeKNH7jubQNm7PAkzlLwEmCs1tfuX3UjIw6vP6QXi+leKh6+LyC/+Ed4EIQermwd58wsyh070yjDHFlNGg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", - "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz", + "integrity": "sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.25.2", - "@babel/helper-validator-option": "^7.24.8", - "browserslist": "^4.23.1", + "@babel/compat-data": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -1044,18 +1072,18 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.4.tgz", - "integrity": "sha512-ro/bFs3/84MDgDmMwbcHgDa8/E6J3QKNTk4xJJnVeFtGE+tL0K26E3pNxhYz2b67fJpt7Aphw5XcploKXuCvCQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.7.tgz", + "integrity": "sha512-bD4WQhbkx80mAyj/WCm4ZHcF4rDxkoLFO6ph8/5/mQ3z4vAzltQXAmbc7GvVJx5H+lk5Mi5EmbTeox5nMGCsbw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-member-expression-to-functions": "^7.24.8", - "@babel/helper-optimise-call-expression": "^7.24.7", - "@babel/helper-replace-supers": "^7.25.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/traverse": "^7.25.4", + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-member-expression-to-functions": "^7.25.7", + "@babel/helper-optimise-call-expression": "^7.25.7", + "@babel/helper-replace-supers": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7", + "@babel/traverse": "^7.25.7", "semver": "^6.3.1" }, "engines": { @@ -1065,15 +1093,28 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz", + "integrity": "sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.2.tgz", - "integrity": "sha512-+wqVGP+DFmqwFD3EH6TMTfUNeqDehV3E/dl+Sd54eaXqm17tEUNbEIn4sVivVowbvUpOtIGxdo3GoXyDH9N/9g==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.7.tgz", + "integrity": "sha512-byHhumTj/X47wJ6C6eLpK7wW/WBEcnUeb7D0FNc/jFQnQVw7DOso3Zz5u9x/zLrFVkHa89ZGDbkAa1D54NdrCQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "regexpu-core": "^5.3.1", + "@babel/helper-annotate-as-pure": "^7.25.7", + "regexpu-core": "^6.1.1", "semver": "^6.3.1" }, "engines": { @@ -1083,6 +1124,19 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz", + "integrity": "sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-define-polyfill-provider": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", @@ -1101,42 +1155,42 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz", - "integrity": "sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.7.tgz", + "integrity": "sha512-O31Ssjd5K6lPbTX9AAYpSKrZmLeagt9uwschJd+Ixo6QiRyfpvgtVQp8qrDR9UNFjZ8+DO34ZkdrN+BnPXemeA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.8", - "@babel/types": "^7.24.8" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz", + "integrity": "sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", - "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.7.tgz", + "integrity": "sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "@babel/traverse": "^7.25.2" + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-simple-access": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "@babel/traverse": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1146,37 +1200,37 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", - "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.7.tgz", + "integrity": "sha512-VAwcwuYhv/AT+Vfr28c9y6SHzTan1ryqrydSTFGjU0uDJHw3uZ+PduI8plCLkRsDnqK2DMEDmwrOQRsK/Ykjng==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.24.7" + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", - "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz", + "integrity": "sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.0.tgz", - "integrity": "sha512-NhavI2eWEIz/H9dbrG0TuOicDhNexze43i5z7lEqwYm0WEZVTwnPpA0EafUTP7+6/W79HWIP2cTe3Z5NiSTVpw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.7.tgz", + "integrity": "sha512-kRGE89hLnPfcz6fTrlNU+uhgcwv0mBE4Gv3P9Ke9kLVJYpi4AMVVEElXvB5CabrPZW4nCM8P8UyyjrzCM0O2sw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-wrap-function": "^7.25.0", - "@babel/traverse": "^7.25.0" + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-wrap-function": "^7.25.7", + "@babel/traverse": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1185,16 +1239,29 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz", + "integrity": "sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-replace-supers": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz", - "integrity": "sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.7.tgz", + "integrity": "sha512-iy8JhqlUW9PtZkd4pHM96v6BdJ66Ba9yWSE4z0W4TvSZwLBPkyDsiIU3ENe4SmrzRBs76F7rQXTy1lYC49n6Lw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.24.8", - "@babel/helper-optimise-call-expression": "^7.24.7", - "@babel/traverse": "^7.25.0" + "@babel/helper-member-expression-to-functions": "^7.25.7", + "@babel/helper-optimise-call-expression": "^7.25.7", + "@babel/traverse": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1204,27 +1271,27 @@ } }, "node_modules/@babel/helper-simple-access": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", - "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.7.tgz", + "integrity": "sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", - "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.7.tgz", + "integrity": "sha512-pPbNbchZBkPMD50K0p3JGcFMNLVUCuU/ABybm/PGNj4JiHrpmNyqqCphBk4i19xXtNV0JhldQJJtbSW5aUvbyA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1244,67 +1311,67 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", + "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", + "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", - "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz", + "integrity": "sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.0.tgz", - "integrity": "sha512-s6Q1ebqutSiZnEjaofc/UKDyC4SbzV5n5SrA2Gq8UawLycr3i04f1dX4OzoQVnexm6aOCh37SQNYlJ/8Ku+PMQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.7.tgz", + "integrity": "sha512-MA0roW3JF2bD1ptAaJnvcabsVlNQShUaThyJbCDD4bCp8NEgiFvpoqRI2YS22hHlc2thjO/fTg2ShLMC3jygAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.25.0", - "@babel/traverse": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/template": "^7.25.7", + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz", - "integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.7.tgz", + "integrity": "sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA==", "license": "MIT", "dependencies": { - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.6" + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", + "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", + "@babel/helper-validator-identifier": "^7.25.7", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" @@ -1314,12 +1381,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", - "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.8.tgz", + "integrity": "sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==", "license": "MIT", "dependencies": { - "@babel/types": "^7.25.6" + "@babel/types": "^7.25.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -1329,14 +1396,14 @@ } }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.3.tgz", - "integrity": "sha512-wUrcsxZg6rqBXG05HG1FPYgsP6EvwF4WpBbxIpWIIYnH8wG0gzx3yZY3dtEHas4sTAOGkbTsc9EGPxwff8lRoA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.7.tgz", + "integrity": "sha512-UV9Lg53zyebzD1DwQoT9mzkEKa922LNUp5YkTJ6Uta0RbyXaQNUgcvSt7qIu1PpPzVb6rd10OVNTzkyBGeVmxQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/traverse": "^7.25.3" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/traverse": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1346,13 +1413,13 @@ } }, "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.0.tgz", - "integrity": "sha512-Bm4bH2qsX880b/3ziJ8KD711LT7z4u8CFudmjqle65AZj/HNUFhEf90dqYv6O86buWvSBmeQDjv0Tn2aF/bIBA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.7.tgz", + "integrity": "sha512-GDDWeVLNxRIkQTnJn2pDOM1pkCgYdSqPeT1a9vh9yIqu2uzzgw1zcqEb+IJOhy+dTBMlNdThrDIksr2o09qrrQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1362,13 +1429,13 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.0.tgz", - "integrity": "sha512-lXwdNZtTmeVOOFtwM/WDe7yg1PL8sYhRk/XH0FzbR2HDQ0xC+EnQ/JHeoMYSavtU115tnUk0q9CDyq8si+LMAA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.7.tgz", + "integrity": "sha512-wxyWg2RYaSUYgmd9MR0FyRGyeOMQE/Uzr1wzd/g5cf5bwi9A4v6HFdDm7y1MgDtod/fLOSTZY6jDgV0xU9d5bA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1378,15 +1445,15 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz", - "integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.7.tgz", + "integrity": "sha512-Xwg6tZpLxc4iQjorYsyGMyfJE7nP5MV8t/Ka58BgiA7Jw0fRqQNcANlLfdJ/yvBt9z9LD2We+BEkT7vLqZRWng==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7", + "@babel/plugin-transform-optional-chaining": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1396,14 +1463,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.0.tgz", - "integrity": "sha512-tggFrk1AIShG/RUQbEwt2Tr/E+ObkfwrPjR6BjbRvsx24+PSjK8zrq0GWPNCjo8qpRx4DuJzlcvWJqlm+0h3kw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.7.tgz", + "integrity": "sha512-UVATLMidXrnH+GMUIuxq55nejlj02HP7F5ETyBONzP6G87fPBogG4CH6kxrSrdIuAjdwNO9VzyaYsrZPscWUrw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/traverse": "^7.25.0" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/traverse": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1507,13 +1574,13 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.6.tgz", - "integrity": "sha512-aABl0jHw9bZ2karQ/uUD6XP4u0SG22SJrOHFoL6XB1R7dTovOP4TzTlsxOYC5yQ1pdscVK2JTUnF6QL3ARoAiQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.7.tgz", + "integrity": "sha512-ZvZQRmME0zfJnDQnVBKYzHxXT7lYBB3Revz1GuS7oLXWMgqUPX4G+DDbT30ICClht9WKV34QVrZhSw6WdklwZQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1565,12 +1632,12 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", - "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.7.tgz", + "integrity": "sha512-ruZOnKO+ajVL/MVx+PwNBPOkrnXTXoWMtte1MBpegfCArhqOe3Bj52avVj1huLLxNKYKXYaSxZ2F+woK1ekXfw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1690,13 +1757,13 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.4.tgz", - "integrity": "sha512-uMOCoHVU52BsSWxPOMVv5qKRdeSlPuImUCB2dlPuBSU+W2/ROE7/Zg8F2Kepbk+8yBa68LlRKxO+xgEVWorsDg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.7.tgz", + "integrity": "sha512-rR+5FDjpCHqqZN2bzZm18bVYGaejGq5ZkpVCJLXor/+zlSrSoc4KWcHI0URVWjl/68Dyr1uwZUz/1njycEAv9g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1723,13 +1790,13 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz", - "integrity": "sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.7.tgz", + "integrity": "sha512-EJN2mKxDwfOUCPxMO6MUI58RN3ganiRAG/MS/S3HfB6QFNjroAMelQo/gybyYq97WerCBAZoyrAoW8Tzdq2jWg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1776,13 +1843,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz", - "integrity": "sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.7.tgz", + "integrity": "sha512-xHttvIM9fvqW+0a3tZlYcZYSBpSWzGBFIt/sYG3tcdSzBB8ZeVgz2gBP7Df+sM0N1850jrviYSSeUuc+135dmQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1792,13 +1859,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.0.tgz", - "integrity": "sha512-yBQjYoOjXlFv9nlXb3f1casSHOZkWr29NX+zChVanLg5Nc157CrbEX9D7hxxtTpuFy7Q0YzmmWfJxzvps4kXrQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.7.tgz", + "integrity": "sha512-ZEPJSkVZaeTFG/m2PARwLZQ+OG0vFIhPlKHK/JdIMy8DbRJ/htz6LRrTFtdzxi9EHmcwbNPAKDnadpNSIW+Aow==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1808,14 +1875,14 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.4.tgz", - "integrity": "sha512-nZeZHyCWPfjkdU5pA/uHiTaDAFUEqkpzf1YoQT2NeSynCGYq9rxfyI3XpQbfx/a0hSnFH6TGlEXvae5Vi7GD8g==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.7.tgz", + "integrity": "sha512-mhyfEW4gufjIqYFo9krXHJ3ElbFLIze5IDp+wQTxoPd+mwFb1NxatNAwmv8Q8Iuxv7Zc+q8EkiMQwc9IhyGf4g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.4", - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1825,15 +1892,14 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz", - "integrity": "sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.25.8.tgz", + "integrity": "sha512-e82gl3TCorath6YLf9xUwFehVvjvfqFhdOo4+0iVIVju+6XOi5XHkqB3P2AXnSwoeTX0HBoXq5gJFtvotJzFnQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-class-static-block": "^7.14.5" + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1843,17 +1909,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.4.tgz", - "integrity": "sha512-oexUfaQle2pF/b6E0dwsxQtAol9TLSO88kQvym6HHBWFliV2lGdrPieX+WgMRLSJDVzdYywk7jXbLPuO2KLTLg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.7.tgz", + "integrity": "sha512-9j9rnl+YCQY0IGoeipXvnk3niWicIB6kCsWRGLwX241qSXpbA4MKxtp/EdvFxsc4zI5vqfLxzOd0twIJ7I99zg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-compilation-targets": "^7.25.2", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-replace-supers": "^7.25.0", - "@babel/traverse": "^7.25.4", + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-replace-supers": "^7.25.7", + "@babel/traverse": "^7.25.7", "globals": "^11.1.0" }, "engines": { @@ -1863,15 +1929,28 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz", + "integrity": "sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz", - "integrity": "sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.7.tgz", + "integrity": "sha512-QIv+imtM+EtNxg/XBKL3hiWjgdLjMOmZ+XzQwSgmBfKbfxUjBzGgVPklUuE55eq5/uVoh8gg3dqlrwR/jw3ZeA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/template": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/template": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1881,13 +1960,13 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz", - "integrity": "sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.7.tgz", + "integrity": "sha512-xKcfLTlJYUczdaM1+epcdh1UGewJqr9zATgrNHcLBcV2QmfvPPEixo/sK/syql9cEmbr7ulu5HMFG5vbbt/sEA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1897,14 +1976,14 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz", - "integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.7.tgz", + "integrity": "sha512-kXzXMMRzAtJdDEgQBLF4oaiT6ZCU3oWHgpARnTKDAqPkDJ+bs3NrZb310YYevR5QlRo3Kn7dzzIdHbZm1VzJdQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1914,13 +1993,13 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz", - "integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.7.tgz", + "integrity": "sha512-by+v2CjoL3aMnWDOyCIg+yxU9KXSRa9tN6MbqggH5xvymmr9p4AMjYkNlQy4brMceBnUyHZ9G8RnpvT8wP7Cfg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1930,14 +2009,14 @@ } }, "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.0.tgz", - "integrity": "sha512-YLpb4LlYSc3sCUa35un84poXoraOiQucUTTu8X1j18JV+gNa8E0nyUf/CjZ171IRGr4jEguF+vzJU66QZhn29g==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.7.tgz", + "integrity": "sha512-HvS6JF66xSS5rNKXLqkk7L9c/jZ/cdIVIcoPVrnl8IsVpLggTjXs8OWekbLHs/VtYDDh5WXnQyeE3PPUGm22MA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.0", - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1947,14 +2026,13 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz", - "integrity": "sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.8.tgz", + "integrity": "sha512-gznWY+mr4ZQL/EWPcbBQUP3BXS5FwZp8RUOw06BaRn8tQLzN4XLIxXejpHN9Qo8x8jjBmAAKp6FoS51AgkSA/A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1964,14 +2042,14 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz", - "integrity": "sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.25.7.tgz", + "integrity": "sha512-yjqtpstPfZ0h/y40fAXRv2snciYr0OAoMXY/0ClC7tm4C/nG5NJKmIItlaYlLbIVAWNfrYuy9dq1bE0SbX0PEg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1981,14 +2059,13 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz", - "integrity": "sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.8.tgz", + "integrity": "sha512-sPtYrduWINTQTW7FtOy99VCTWp4H23UX7vYcut7S4CIMEXU+54zKX9uCoGkLsWXteyaMXzVHgzWbLfQ1w4GZgw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1998,14 +2075,14 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz", - "integrity": "sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.7.tgz", + "integrity": "sha512-n/TaiBGJxYFWvpJDfsxSj9lEEE44BFM1EPGz4KEiTipTgkoFVVcCmzAL3qA7fdQU96dpo4gGf5HBx/KnDvqiHw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2015,15 +2092,15 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.25.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.1.tgz", - "integrity": "sha512-TVVJVdW9RKMNgJJlLtHsKDTydjZAbwIsn6ySBPQaEAUU5+gVvlJt/9nRmqVbsV/IBanRjzWoaAQKLoamWVOUuA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.7.tgz", + "integrity": "sha512-5MCTNcjCMxQ63Tdu9rxyN6cAWurqfrDZ76qvVPrGYdBxIj+EawuuxTu/+dgJlhK5eRz3v1gLwp6XwS8XaX2NiQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.24.8", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/traverse": "^7.25.1" + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/traverse": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2033,14 +2110,13 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz", - "integrity": "sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.8.tgz", + "integrity": "sha512-4OMNv7eHTmJ2YXs3tvxAfa/I43di+VcF+M4Wt66c88EAED1RoGaf1D64cL5FkRpNL+Vx9Hds84lksWvd/wMIdA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-json-strings": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2050,13 +2126,13 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.2.tgz", - "integrity": "sha512-HQI+HcTbm9ur3Z2DkO+jgESMAMcYLuN/A7NRw9juzxAezN9AvqvUTnpKP/9kkYANz6u7dFlAyOu44ejuGySlfw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.7.tgz", + "integrity": "sha512-fwzkLrSu2fESR/cm4t6vqd7ebNIopz2QHGtjoU+dswQo/P6lwAG04Q98lliE3jkz/XqnbGFLnUcE0q0CVUf92w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2066,14 +2142,13 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz", - "integrity": "sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.8.tgz", + "integrity": "sha512-f5W0AhSbbI+yY6VakT04jmxdxz+WsID0neG7+kQZbCOjuyJNdL5Nn4WIBm4hRpKnUcO9lP0eipUhFN12JpoH8g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2083,13 +2158,13 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz", - "integrity": "sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.7.tgz", + "integrity": "sha512-Std3kXwpXfRV0QtQy5JJcRpkqP8/wG4XL7hSKZmGlxPlDqmpXtEPRmhF7ztnlTCtUN3eXRUJp+sBEZjaIBVYaw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2099,14 +2174,14 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz", - "integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.7.tgz", + "integrity": "sha512-CgselSGCGzjQvKzghCvDTxKHP3iooenLpJDO842ehn5D2G5fJB222ptnDwQho0WjEvg7zyoxb9P+wiYxiJX5yA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2116,15 +2191,15 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz", - "integrity": "sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.7.tgz", + "integrity": "sha512-L9Gcahi0kKFYXvweO6n0wc3ZG1ChpSFdgG+eV1WYZ3/dGbJK7vvk91FgGgak8YwRgrCuihF8tE/Xg07EkL5COg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.24.8", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-simple-access": "^7.24.7" + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-simple-access": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2134,16 +2209,16 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.0.tgz", - "integrity": "sha512-YPJfjQPDXxyQWg/0+jHKj1llnY5f/R6a0p/vP4lPymxLu7Lvl4k2WMitqi08yxwQcCVUUdG9LCUj4TNEgAp3Jw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.7.tgz", + "integrity": "sha512-t9jZIvBmOXJsiuyOwhrIGs8dVcD6jDyg2icw1VL4A/g+FnWyJKwUfSSU2nwJuMV2Zqui856El9u+ElB+j9fV1g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.0", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "@babel/traverse": "^7.25.0" + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "@babel/traverse": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2153,14 +2228,14 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz", - "integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.7.tgz", + "integrity": "sha512-p88Jg6QqsaPh+EB7I9GJrIqi1Zt4ZBHUQtjw3z1bzEXcLh6GfPqzZJ6G+G1HBGKUNukT58MnKG7EN7zXQBCODw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2170,14 +2245,14 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz", - "integrity": "sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.7.tgz", + "integrity": "sha512-BtAT9LzCISKG3Dsdw5uso4oV1+v2NlVXIIomKJgQybotJY3OwCwJmkongjHgwGKoZXd0qG5UZ12JUlDQ07W6Ow==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2187,13 +2262,13 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz", - "integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.7.tgz", + "integrity": "sha512-CfCS2jDsbcZaVYxRFo2qtavW8SpdzmBXC2LOI4oO0rP+JSRDxxF3inF4GcPsLgfb5FjkhXG5/yR/lxuRs2pySA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2203,14 +2278,13 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz", - "integrity": "sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.8.tgz", + "integrity": "sha512-Z7WJJWdQc8yCWgAmjI3hyC+5PXIubH9yRKzkl9ZEG647O9szl9zvmKLzpbItlijBnVhTUf1cpyWBsZ3+2wjWPQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2220,14 +2294,13 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz", - "integrity": "sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.8.tgz", + "integrity": "sha512-rm9a5iEFPS4iMIy+/A/PiS0QN0UyjPIeVvbU5EMZFKJZHt8vQnasbpo3T3EFcxzCeYO0BHfc4RqooCZc51J86Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2237,16 +2310,15 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz", - "integrity": "sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.8.tgz", + "integrity": "sha512-LkUu0O2hnUKHKE7/zYOIjByMa4VRaV2CD/cdGz0AxU9we+VA3kDDggKEzI0Oz1IroG+6gUP6UmWEHBMWZU316g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.24.7" + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/plugin-transform-parameters": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2256,14 +2328,14 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz", - "integrity": "sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.7.tgz", + "integrity": "sha512-pWT6UXCEW3u1t2tcAGtE15ornCBvopHj9Bps9D2DsH15APgNVOTwwczGckX+WkAvBmuoYKRCFa4DK+jM8vh5AA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-replace-supers": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-replace-supers": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2273,14 +2345,13 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz", - "integrity": "sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.8.tgz", + "integrity": "sha512-EbQYweoMAHOn7iJ9GgZo14ghhb9tTjgOc88xFgYngifx7Z9u580cENCV159M4xDh3q/irbhSjZVpuhpC2gKBbg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2290,15 +2361,14 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz", - "integrity": "sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.8.tgz", + "integrity": "sha512-q05Bk7gXOxpTHoQ8RSzGSh/LHVB9JEIkKnk3myAWwZHnYiTGYtbdrYkIsS8Xyh4ltKf7GNUSgzs/6P2bJtBAQg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2308,13 +2378,13 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz", - "integrity": "sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.7.tgz", + "integrity": "sha512-FYiTvku63me9+1Nz7TOx4YMtW3tWXzfANZtrzHhUZrz4d47EEtMQhzFoZWESfXuAMMT5mwzD4+y1N8ONAX6lMQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2324,14 +2394,14 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.4.tgz", - "integrity": "sha512-ao8BG7E2b/URaUQGqN3Tlsg+M3KlHY6rJ1O1gXAEUnZoyNQnvKyH87Kfg+FoxSeyWUB8ISZZsC91C44ZuBFytw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.7.tgz", + "integrity": "sha512-KY0hh2FluNxMLwOCHbxVOKfdB5sjWG4M183885FmaqWWiGMhRZq4DQRKH6mHdEucbJnyDyYiZNwNG424RymJjA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.4", - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2341,16 +2411,15 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz", - "integrity": "sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.8.tgz", + "integrity": "sha512-8Uh966svuB4V8RHHg0QJOB32QK287NBksJOByoKmHMp1TAobNniNalIkI2i5IPj5+S9NYCG4VIjbEuiSN8r+ow==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2359,14 +2428,27 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-private-property-in-object/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz", + "integrity": "sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz", - "integrity": "sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.7.tgz", + "integrity": "sha512-lQEeetGKfFi0wHbt8ClQrUSUMfEeI3MMm74Z73T9/kuz990yYVtfofjf3NuA42Jy3auFOpbjDyCSiIkTs1VIYw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2376,13 +2458,13 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz", - "integrity": "sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.7.tgz", + "integrity": "sha512-mgDoQCRjrY3XK95UuV60tZlFCQGXEtMg8H+IsW72ldw1ih1jZhzYXbJvghmAEpg5UVhhnCeia1CkGttUvCkiMQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-plugin-utils": "^7.25.7", "regenerator-transform": "^0.15.2" }, "engines": { @@ -2393,13 +2475,13 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz", - "integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.7.tgz", + "integrity": "sha512-3OfyfRRqiGeOvIWSagcwUTVk2hXBsr/ww7bLn6TRTuXnexA+Udov2icFOxFX9abaj4l96ooYkcNN1qi2Zvqwng==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2430,13 +2512,13 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz", - "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.7.tgz", + "integrity": "sha512-uBbxNwimHi5Bv3hUccmOFlUy3ATO6WagTApenHz9KzoIdn0XeACdB12ZJ4cjhuB2WSi80Ez2FWzJnarccriJeA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2446,14 +2528,14 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz", - "integrity": "sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.7.tgz", + "integrity": "sha512-Mm6aeymI0PBh44xNIv/qvo8nmbkpZze1KvR8MkEqbIREDxoiWTi18Zr2jryfRMwDfVZF9foKh060fWgni44luw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2463,13 +2545,13 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz", - "integrity": "sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.7.tgz", + "integrity": "sha512-ZFAeNkpGuLnAQ/NCsXJ6xik7Id+tHuS+NT+ue/2+rn/31zcdnupCdmunOizEaP0JsUmTFSTOPoQY7PkK2pttXw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2479,13 +2561,13 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz", - "integrity": "sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.7.tgz", + "integrity": "sha512-SI274k0nUsFFmyQupiO7+wKATAmMFf8iFgq2O+vVFXZ0SV9lNfT1NGzBEhjquFmD8I9sqHLguH+gZVN3vww2AA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2495,13 +2577,13 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz", - "integrity": "sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.7.tgz", + "integrity": "sha512-OmWmQtTHnO8RSUbL0NTdtpbZHeNTnm68Gj5pA4Y2blFNh+V4iZR68V1qL9cI37J21ZN7AaCnkfdHtLExQPf2uA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2511,13 +2593,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz", - "integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.7.tgz", + "integrity": "sha512-BN87D7KpbdiABA+t3HbVqHzKWUDN3dymLaTnPFAMyc8lV+KN3+YzNhVRNdinaCPA4AUqx7ubXbQ9shRjYBl3SQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2527,14 +2609,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz", - "integrity": "sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.7.tgz", + "integrity": "sha512-IWfR89zcEPQGB/iB408uGtSPlQd3Jpq11Im86vUgcmSTcoWAiQMCTOa2K2yNNqFJEBVICKhayctee65Ka8OB0w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2544,14 +2626,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz", - "integrity": "sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.7.tgz", + "integrity": "sha512-8JKfg/hiuA3qXnlLx8qtv5HWRbgyFx2hMMtpDDuU2rTckpKkGu4ycK5yYHwuEa16/quXfoxHBIApEsNyMWnt0g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2561,14 +2643,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.4.tgz", - "integrity": "sha512-qesBxiWkgN1Q+31xUE9RcMk79eOXXDCv6tfyGMRSs4RGlioSg2WVyQAm07k726cSE56pa+Kb0y9epX2qaXzTvA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.7.tgz", + "integrity": "sha512-YRW8o9vzImwmh4Q3Rffd09bH5/hvY0pxg+1H1i0f7APoUeg12G7+HhLj9ZFNIrYkgBXhIijPJ+IXypN0hLTIbw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.2", - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2690,13 +2772,6 @@ "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@babel/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", - "dev": true, - "license": "MIT" - }, "node_modules/@babel/runtime": { "version": "7.25.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", @@ -2710,30 +2785,30 @@ } }, "node_modules/@babel/template": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", - "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.7.tgz", + "integrity": "sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/code-frame": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", - "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.7.tgz", + "integrity": "sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.6", - "@babel/parser": "^7.25.6", - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.6", + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -2742,28 +2817,40 @@ } }, "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", - "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.7.tgz", + "integrity": "sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.25.6", + "@babel/types": "^7.25.7", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/traverse/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@babel/types": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", - "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.8.tgz", + "integrity": "sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", + "@babel/helper-string-parser": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -2838,9 +2925,9 @@ } }, "node_modules/@emnapi/core": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.2.0.tgz", - "integrity": "sha512-E7Vgw78I93we4ZWdYCb4DGAwRROGkMIXk7/y87UmANR+J6qsWusmC3gLt0H+O0KOt5e6O38U8oJamgbudrES/w==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.3.1.tgz", + "integrity": "sha512-pVGjBIt1Y6gg3EJN8jTcfpP/+uuRksIo055oE/OBkDNcjZqVbfkWCksG1Jp4yZnj3iKWyWX8fdG/j6UDYPbFog==", "dev": true, "license": "MIT", "dependencies": { @@ -2849,9 +2936,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz", - "integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", + "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", "dev": true, "license": "MIT", "dependencies": { @@ -3464,9 +3551,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.11.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.1.tgz", - "integrity": "sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==", + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.12.0.tgz", + "integrity": "sha512-eohesHH8WFRUprDNyEREgqP6beG6htMeUYeCpkEgBCieCMme5r9zFWjzAJp//9S+Kub4rqE+jXe9Cp1a7IYIIA==", "dev": true, "license": "MIT", "engines": { @@ -3563,6 +3650,30 @@ "node": ">=6" } }, + "node_modules/@humanfs/core": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz", + "integrity": "sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.5.tgz", + "integrity": "sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.0", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -3578,9 +3689,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", - "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3699,9 +3810,9 @@ } }, "node_modules/@inquirer/figures": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.6.tgz", - "integrity": "sha512-yfZzps3Cso2UbM7WlxKwZQh2Hs6plrbjs1QnzQDZhK2DgyCo6D8AaHps9olkNcUFlcYERMqU3uJSp1gmy3s/qQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.7.tgz", + "integrity": "sha512-m+Trk77mp54Zma6xLkLuY+mvanPxlE4A7yNKs2HBiyZ4UkVs28Mv5c/pgWrHeInx+USHeX/WEPzjrWrcJiQgjw==", "dev": true, "license": "MIT", "engines": { @@ -4896,9 +5007,9 @@ } }, "node_modules/@jsonjoy.com/util": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.3.0.tgz", - "integrity": "sha512-Cebt4Vk7k1xHy87kHY7KSPLT77A7Ev7IfOblyLZhtYEhrdQ6fX4EoLq3xOQ3O/DRMEh2ok5nyC180E+ABS8Wmw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.5.0.tgz", + "integrity": "sha512-ojoNsrIuPI9g6o8UxhraZQSyF2ByJanAY4cTFbc8Mf2AXEF4aQRGY1dJxyJpuyav8r9FGflEt/Ff3u5Nt6YMPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5265,9 +5376,9 @@ } }, "node_modules/@ngtools/webpack": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.6.tgz", - "integrity": "sha512-7HwOPE1EOgcHnpt4brSiT8G2CcXB50G0+CbCBaKGy4LYCG3Y3mrlzF5Fup9HvMJ6Tzqd62RqzpKKYBiGUT7hxg==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.8.tgz", + "integrity": "sha512-sq0kI8gEen4QlM6X8XqOYy7j4B8iLCYNo+iKxatV36ts4AXH0MuVkP56+oMaoH5oZNoSqd0RlfnotEHfvJAr8A==", "dev": true, "license": "MIT", "engines": { @@ -5597,23 +5708,23 @@ } }, "node_modules/@nrwl/devkit": { - "version": "19.6.5", - "resolved": "https://registry.npmjs.org/@nrwl/devkit/-/devkit-19.6.5.tgz", - "integrity": "sha512-KaQeVyYaWBQwQSITtumPvx+P7IpKFReETx4gLTcOpQ/a3QD/AZFGbNjiG+xDLbgo1FDh9dRt9k7eWhGk6oPWKQ==", + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/@nrwl/devkit/-/devkit-19.8.4.tgz", + "integrity": "sha512-OoIqDjj2mWzLs3aSF6w5OiC2xywYi/jBxHc7t7Lyi56Vc4dQq8vJMELa9WtG6qH0k05fF7N+jAoKlfvLgbbEFA==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "19.6.5" + "@nx/devkit": "19.8.4" } }, "node_modules/@nrwl/tao": { - "version": "19.6.5", - "resolved": "https://registry.npmjs.org/@nrwl/tao/-/tao-19.6.5.tgz", - "integrity": "sha512-EoUN/kE6CMWJ4ZZgcXAyiOzn8BSshG2DhC5PNwzLTAxRBus8FgXR/9c0XOzchaP46Kq3hoBGFgeyW434tfuv5w==", + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/@nrwl/tao/-/tao-19.8.4.tgz", + "integrity": "sha512-03/+QZ4/6HmKbEmvzCutLI1XIclBspNYtiVHmGPRWuwhnZViqYfnyl8J7RWVdFEoKKA5fhJqpg7e28aGuoMBvQ==", "dev": true, "license": "MIT", "dependencies": { - "nx": "19.6.5", + "nx": "19.8.4", "tslib": "^2.3.0" }, "bin": { @@ -5621,13 +5732,13 @@ } }, "node_modules/@nx/devkit": { - "version": "19.6.5", - "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-19.6.5.tgz", - "integrity": "sha512-AEaMSr55Ar48QHU8TBi/gzLtjeT100zdyfLmk0RoiLzjjC8pWmm3Xfvqxyt1WsUUf4oQhlHlolJuoM41qKsdZw==", + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-19.8.4.tgz", + "integrity": "sha512-FPFT8gVDFRSEmU0n7nRkT4Rnqy7OMznfPXLfDZtVuzEi5Cl6ftG3UBUvCgJcJFCYJVAZAUuv6vRSRarHd51XFQ==", "dev": true, "license": "MIT", "dependencies": { - "@nrwl/devkit": "19.6.5", + "@nrwl/devkit": "19.8.4", "ejs": "^3.1.7", "enquirer": "~2.3.6", "ignore": "^5.0.4", @@ -5668,9 +5779,9 @@ } }, "node_modules/@nx/nx-darwin-arm64": { - "version": "19.6.5", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-19.6.5.tgz", - "integrity": "sha512-sFU2k0BaklM17206F2E5C3866y0SICb0xyuPeD6D07a6hB4IstjIUkldUJJN70wEsJ5I3VP4yZ2oJcwnb1TTRQ==", + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-19.8.4.tgz", + "integrity": "sha512-mbSGt63hYcVCSQ54kpHl0lFqr5CsbkGJ4L3liWE30Da7vXZJwUBr9f+b9DnQ64IZzlu6vAhNcaiYQXa9lAk0yQ==", "cpu": [ "arm64" ], @@ -5685,9 +5796,9 @@ } }, "node_modules/@nx/nx-darwin-x64": { - "version": "19.6.5", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-19.6.5.tgz", - "integrity": "sha512-EJmTbUPmlksgOap6xkQl89+zXwHpaAnZLsyLHUd7i00eVRa21FRhdKFnVsRxtwPDZp/YCG84IzMUye/IrwDFTQ==", + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-19.8.4.tgz", + "integrity": "sha512-lTcXUCXNvqHdLmrNCOyDF+u6pDx209Ew7nSR47sQPvkycIHYi0gvgk0yndFn1Swah0lP4OxWg7rzAfmOlZd6ew==", "cpu": [ "x64" ], @@ -5702,9 +5813,9 @@ } }, "node_modules/@nx/nx-freebsd-x64": { - "version": "19.6.5", - "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-19.6.5.tgz", - "integrity": "sha512-rR8NJCskoEmIbK96uxaevHm146WDTA0V3jId+X1joITqjj3E2DMm0U4r5v/OgI5+iqbhFV4S83LrMxP6gBLTsQ==", + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-19.8.4.tgz", + "integrity": "sha512-4BUplOxPZeUwlUNfzHHMmebNVgDFW/jNX6TWRS+jINwOHnpWLkLFAXu27G80/S3OaniVCzEQklXO9b+1UsdgXw==", "cpu": [ "x64" ], @@ -5718,12 +5829,270 @@ "node": ">= 10" } }, - "node_modules/@nx/nx-linux-arm-gnueabihf": { - "version": "19.6.5", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-19.6.5.tgz", - "integrity": "sha512-OUHFV6iLlJN7b7qFnqLfa0Yj/aoylEiRXcEhV1bhPm0Ryt1bOeGDmLYScVN8n5t+AVmrwwYHk+ajXMzCOLLeZw==", + "node_modules/@nx/nx-linux-arm-gnueabihf": { + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-19.8.4.tgz", + "integrity": "sha512-Wahul8oz9huEm/Jv3wud5IGWdZxkGG4tdJm9i5TV5wxfUMAWbKU9v2nzZZins452UYESWvwvDkiuBPZqSto3qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-arm64-gnu": { + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-19.8.4.tgz", + "integrity": "sha512-L0RVCZkNAtZDplLT7uJV7M9cXxq2Fxw+8ex3eb9XSp7eyLeFO21T0R6vTouJ42E/PEvGApCAcyGqtnyPNMZFfw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-arm64-musl": { + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-19.8.4.tgz", + "integrity": "sha512-0q8r8I8WCsY3xowDI2j109SCUSkFns/BJ40aCfRh9hhrtaIIc5qXUw2YFTjxUZNcRJXx9j9+hTe9jBkUSIGvCw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-x64-gnu": { + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-19.8.4.tgz", + "integrity": "sha512-XcRBNe0ws7KB0PMcUlpQqzzjjxMP8VdqirBz7CfB2XQ8xKmP3370p0cDvqs/4oKDHK4PCkmvVFX60tzakutylA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-x64-musl": { + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-19.8.4.tgz", + "integrity": "sha512-JB4tAuZBCF0yqSnKF3pHXa0b7LA3ebi3Bw08QmMr//ON4aU+eXURGBuj9XvULD2prY+gpBrvf+MsG1XJAHL6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-win32-arm64-msvc": { + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-19.8.4.tgz", + "integrity": "sha512-WvQag/pN9ofRWRDvOZxj3jvJoTetlvV1uyirnDrhupRgi+Fj67OlGGt2zVUHaXFGEa1MfCEG6Vhk6152m4KyaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-win32-x64-msvc": { + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-19.8.4.tgz", + "integrity": "sha512-//JntLrN3L7WL/WgP3D0FE34caYTPcG/GIMBguC9w7YDyTlEikLgLbobjdCPz+2f9OWGvIZbJgGmtHNjnETM/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz", + "integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.4.1", + "@parcel/watcher-darwin-arm64": "2.4.1", + "@parcel/watcher-darwin-x64": "2.4.1", + "@parcel/watcher-freebsd-x64": "2.4.1", + "@parcel/watcher-linux-arm-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-musl": "2.4.1", + "@parcel/watcher-linux-x64-glibc": "2.4.1", + "@parcel/watcher-linux-x64-musl": "2.4.1", + "@parcel/watcher-win32-arm64": "2.4.1", + "@parcel/watcher-win32-ia32": "2.4.1", + "@parcel/watcher-win32-x64": "2.4.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz", + "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz", + "integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz", + "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz", + "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz", + "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz", + "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==", "cpu": [ - "arm" + "arm64" ], "dev": true, "license": "MIT", @@ -5732,13 +6101,17 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nx/nx-linux-arm64-gnu": { - "version": "19.6.5", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-19.6.5.tgz", - "integrity": "sha512-CzbJfb24poaJgBHt4aKLaL8a7bO9KXCLls+TX0SZfmzA9AWX6YuiX9lhxwBv6cqsViXTDB4KnXndMDB/H0Gk4g==", + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz", + "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==", "cpu": [ "arm64" ], @@ -5749,15 +6122,19 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nx/nx-linux-arm64-musl": { - "version": "19.6.5", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-19.6.5.tgz", - "integrity": "sha512-MgidKilQ0KWxQbTnaqXGjASu7wtAC9q6zAwFNKFENkwJq3nThaQH6jQVlnINE4lL9NSgyyg0AS/ix31hiqAgvA==", + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz", + "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", @@ -5766,13 +6143,17 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nx/nx-linux-x64-gnu": { - "version": "19.6.5", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-19.6.5.tgz", - "integrity": "sha512-rGDylAoslIlk5TDbEJ6YoQOYxxYP9gCpi6FLke2mFgXVzOmVlLKHfVsegIHYVMYYF26h3NJh0NLGGzGdoBjWgQ==", + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz", + "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", "cpu": [ "x64" ], @@ -5783,32 +6164,40 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nx/nx-linux-x64-musl": { - "version": "19.6.5", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-19.6.5.tgz", - "integrity": "sha512-C/pNjDL/bDEcrDypgBo4r1AOiPTk8gWJwBsFE1QHIvg7//5WFSreqRj34rJu/GZ95eLYJH5tje1VW6z+atEGkQ==", + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz", + "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nx/nx-win32-arm64-msvc": { - "version": "19.6.5", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-19.6.5.tgz", - "integrity": "sha512-mMi8i16OFux17xed2iLPWwUdCbS1mYA9Ny/gnoNUCosmihmXX9wrzaGBkNAMsHA28huYQtPhGormsEs+zuiVFg==", + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz", + "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", "cpu": [ - "arm64" + "ia32" ], "dev": true, "license": "MIT", @@ -5817,13 +6206,17 @@ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nx/nx-win32-x64-msvc": { - "version": "19.6.5", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-19.6.5.tgz", - "integrity": "sha512-jjhbDYNBkyz9Fg1jf0KZTrgdf/yx4v+k0ifukDIHZjva+jko0Ve5WzdkQ2K07M9ZxxYibDtTDqX9uX6+eFZtoA==", + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", + "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", "cpu": [ "x64" ], @@ -5834,9 +6227,33 @@ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" } }, + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -6153,14 +6570,14 @@ ] }, "node_modules/@schematics/angular": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.6.tgz", - "integrity": "sha512-Y988EoOEQDLEyHu3414T6AeVUyx21AexBHQNbUNQkK8cxlxyB6m1eH1cx6vFgLRFUTsLVv+C6Ln/ICNTfLcG4A==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.8.tgz", + "integrity": "sha512-62Sr7/j/dlhZorxH4GzQgpJy0s162BVts0Q7knZuEacP4VL+IWOUE1NS9OFkh/cbomoyXBdoewkZ5Zd1dVX78w==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.6", - "@angular-devkit/schematics": "18.2.6", + "@angular-devkit/core": "18.2.8", + "@angular-devkit/schematics": "18.2.8", "jsonc-parser": "3.3.1" }, "engines": { @@ -6170,73 +6587,73 @@ } }, "node_modules/@sentry-internal/browser-utils": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.32.0.tgz", - "integrity": "sha512-DpUGhk5O1OVjT0fo9wsbEdO1R/S9gGBRDtn9+FFVeRtieJHwXpeZiLK+tZhTOvaILmtSoTPUEY3L5sK4j5Xq9g==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.34.0.tgz", + "integrity": "sha512-4AcYOzPzD1tL5eSRQ/GpKv5enquZf4dMVUez99/Bh3va8qiJrNP55AcM7UzZ7WZLTqKygIYruJTU5Zu2SpEAPQ==", "license": "MIT", "dependencies": { - "@sentry/core": "8.32.0", - "@sentry/types": "8.32.0", - "@sentry/utils": "8.32.0" + "@sentry/core": "8.34.0", + "@sentry/types": "8.34.0", + "@sentry/utils": "8.34.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/feedback": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.32.0.tgz", - "integrity": "sha512-XB7hiVJQW1tNzpoXIHbvm3rjipIt7PZiJJtFg2vxaqu/FzdgOcYqQiwIKivJVAKuRZ9rIeJtK1jdXQFOc/TRJA==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.34.0.tgz", + "integrity": "sha512-aYSM2KPUs0FLPxxbJCFSwCYG70VMzlT04xepD1Y/tTlPPOja/02tSv2tyOdZbv8Uw7xslZs3/8Lhj74oYcTBxw==", "license": "MIT", "dependencies": { - "@sentry/core": "8.32.0", - "@sentry/types": "8.32.0", - "@sentry/utils": "8.32.0" + "@sentry/core": "8.34.0", + "@sentry/types": "8.34.0", + "@sentry/utils": "8.34.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/replay": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.32.0.tgz", - "integrity": "sha512-yiEUnn2yyo1AIQIFNeRX3tdK8fmyKIkxdFS1WiVQmeYI/hFwYBTZPly0FcO/g3xnRMSA2tvrS+hZEaaXfK4WhA==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.34.0.tgz", + "integrity": "sha512-EoMh9NYljNewZK1quY23YILgtNdGgrkzJ9TPsj6jXUG0LZ0Q7N7eFWd0xOEDBvFxrmI3cSXF1i4d1sBb+eyKRw==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "8.32.0", - "@sentry/core": "8.32.0", - "@sentry/types": "8.32.0", - "@sentry/utils": "8.32.0" + "@sentry-internal/browser-utils": "8.34.0", + "@sentry/core": "8.34.0", + "@sentry/types": "8.34.0", + "@sentry/utils": "8.34.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/replay-canvas": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.32.0.tgz", - "integrity": "sha512-oBbhtDBkD+5z/T0NVJ5VenBWAid/S9QdVrod/UqxVqU7F8N+E9/INFQI48zCWr4iVlUMcszJPDElvJEsMDvvBQ==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.34.0.tgz", + "integrity": "sha512-x8KhZcCDpbKHqFOykYXiamX6x0LRxv6N1OJHoH+XCrMtiDBZr4Yo30d/MaS6rjmKGMtSRij30v+Uq+YWIgxUrg==", "license": "MIT", "dependencies": { - "@sentry-internal/replay": "8.32.0", - "@sentry/core": "8.32.0", - "@sentry/types": "8.32.0", - "@sentry/utils": "8.32.0" + "@sentry-internal/replay": "8.34.0", + "@sentry/core": "8.34.0", + "@sentry/types": "8.34.0", + "@sentry/utils": "8.34.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry/angular": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@sentry/angular/-/angular-8.32.0.tgz", - "integrity": "sha512-HgdpLFTdAMgTG4yz6mb9umg+yGlCkuRDqC4Wv1zNW7ARoSioavyz4kMRkKqJR6hxgGh2vPoXCz6E+w8L4k9oPg==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@sentry/angular/-/angular-8.34.0.tgz", + "integrity": "sha512-FjBN5s+SFzTFHQh5DqWUGUp19p3V7p86I7Dq1a7MBCzmQukGM1bcW8+n6wLj6CxlEoyLCPPZpTIXIO4ulheIwg==", "license": "MIT", "dependencies": { - "@sentry/browser": "8.32.0", - "@sentry/core": "8.32.0", - "@sentry/types": "8.32.0", - "@sentry/utils": "8.32.0", + "@sentry/browser": "8.34.0", + "@sentry/core": "8.34.0", + "@sentry/types": "8.34.0", + "@sentry/utils": "8.34.0", "tslib": "^2.4.1" }, "engines": { @@ -6250,52 +6667,52 @@ } }, "node_modules/@sentry/browser": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.32.0.tgz", - "integrity": "sha512-AEKFj64g4iYwEMRvVcxiY0FswmClRXCP1IEvCqujn8OBS8AjMOr1z/RwYieEs0D90yNNB3YEqF8adrKENblJmw==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.34.0.tgz", + "integrity": "sha512-3HHG2NXxzHq1lVmDy2uRjYjGNf9NsJsTPlOC70vbQdOb+S49EdH/XMPy+J3ruIoyv6Cu0LwvA6bMOM6rHZOgNQ==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "8.32.0", - "@sentry-internal/feedback": "8.32.0", - "@sentry-internal/replay": "8.32.0", - "@sentry-internal/replay-canvas": "8.32.0", - "@sentry/core": "8.32.0", - "@sentry/types": "8.32.0", - "@sentry/utils": "8.32.0" + "@sentry-internal/browser-utils": "8.34.0", + "@sentry-internal/feedback": "8.34.0", + "@sentry-internal/replay": "8.34.0", + "@sentry-internal/replay-canvas": "8.34.0", + "@sentry/core": "8.34.0", + "@sentry/types": "8.34.0", + "@sentry/utils": "8.34.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry/core": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.32.0.tgz", - "integrity": "sha512-+xidTr0lZ0c755tq4k75dXPEb8PA+qvIefW3U9+dQMORLokBrYoKYMf5zZTG2k/OfSJS6OSxatUj36NFuCs3aA==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.34.0.tgz", + "integrity": "sha512-adrXCTK/zsg5pJ67lgtZqdqHvyx6etMjQW3P82NgWdj83c8fb+zH+K79Z47pD4zQjX0ou2Ws5nwwi4wJbz4bfA==", "license": "MIT", "dependencies": { - "@sentry/types": "8.32.0", - "@sentry/utils": "8.32.0" + "@sentry/types": "8.34.0", + "@sentry/utils": "8.34.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry/types": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.32.0.tgz", - "integrity": "sha512-hxckvN2MzS5SgGDgVQ0/QpZXk13Vrq4BtZLwXhPhyeTmZtUiUfWvcL5TFQqLinfKdTKPe9q2MxeAJ0D4LalhMg==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.34.0.tgz", + "integrity": "sha512-zLRc60CzohGCo6zNsNeQ9JF3SiEeRE4aDCP9fDDdIVCOKovS+mn1rtSip0qd0Vp2fidOu0+2yY0ALCz1A3PJSQ==", "license": "MIT", "engines": { "node": ">=14.18" } }, "node_modules/@sentry/utils": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.32.0.tgz", - "integrity": "sha512-t1WVERhgmYURxbBj9J4/H2P2X+VKqm7B3ce9iQyrZbdf5NekhcU4jHIecPUWCPHjQkFIqkVTorqeBmDTlg/UmQ==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.34.0.tgz", + "integrity": "sha512-W1KoRlFUjprlh3t86DZPFxLfM6mzjRzshVfMY7vRlJFymBelJsnJ3A1lPeBZM9nCraOSiw6GtOWu6k5BAkiGIg==", "license": "MIT", "dependencies": { - "@sentry/types": "8.32.0" + "@sentry/types": "8.34.0" }, "engines": { "node": ">=14.18" @@ -6909,9 +7326,9 @@ "license": "MIT" }, "node_modules/@types/lodash": { - "version": "4.17.9", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.9.tgz", - "integrity": "sha512-w9iWudx1XWOHW5lQRS9iKpK/XuRhnN+0T7HvdCCd802FYkT1AMTnxndJHGrNJwRoRHkslGr4S29tjm1cT7x/7w==", + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ==", "dev": true, "license": "MIT" }, @@ -6943,9 +7360,9 @@ } }, "node_modules/@types/node": { - "version": "22.7.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.3.tgz", - "integrity": "sha512-qXKfhXXqGTyBskvWEzJZPUxSslAiLaB6JGP1ic/XTH9ctGgzdgYguuLP1C601aRTSDNlLb0jbKqXjZ48GNraSA==", + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6993,9 +7410,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.9", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.9.tgz", - "integrity": "sha512-+BpAVyTpJkNWWSSnaLBk6ePpHLOGJKnEQNbINNovPWzvEUyAe3e+/d494QdEh71RekM/qV7lw6jzf1HGrJyAtQ==", + "version": "18.3.11", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.11.tgz", + "integrity": "sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==", "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -7149,17 +7566,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.7.0.tgz", - "integrity": "sha512-RIHOoznhA3CCfSTFiB6kBGLQtB/sox+pJ6jeFu6FxJvqL8qRxq/FfGO/UhsGgQM9oGdXkV4xUgli+dt26biB6A==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.1.tgz", + "integrity": "sha512-xfvdgA8AP/vxHgtgU310+WBnLB4uJQ9XdyP17RebG26rLtDrQJV3ZYrcopX91GrHmMoH8bdSwMRh2a//TiJ1jQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.7.0", - "@typescript-eslint/type-utils": "8.7.0", - "@typescript-eslint/utils": "8.7.0", - "@typescript-eslint/visitor-keys": "8.7.0", + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/type-utils": "8.8.1", + "@typescript-eslint/utils": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -7183,16 +7600,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.7.0.tgz", - "integrity": "sha512-lN0btVpj2unxHlNYLI//BQ7nzbMJYBVQX5+pbNXvGYazdlgYonMn4AhhHifQ+J4fGRYA/m1DjaQjx+fDetqBOQ==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.1.tgz", + "integrity": "sha512-hQUVn2Lij2NAxVFEdvIGxT9gP1tq2yM83m+by3whWFsWC+1y8pxxxHUFE1UqDu2VsGi2i6RLcv4QvouM84U+ow==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.7.0", - "@typescript-eslint/types": "8.7.0", - "@typescript-eslint/typescript-estree": "8.7.0", - "@typescript-eslint/visitor-keys": "8.7.0", + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/typescript-estree": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", "debug": "^4.3.4" }, "engines": { @@ -7212,14 +7629,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.7.0.tgz", - "integrity": "sha512-87rC0k3ZlDOuz82zzXRtQ7Akv3GKhHs0ti4YcbAJtaomllXoSO8hi7Ix3ccEvCd824dy9aIX+j3d2UMAfCtVpg==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.1.tgz", + "integrity": "sha512-X4JdU+66Mazev/J0gfXlcC/dV6JI37h+93W9BRYXrSn0hrE64IoWgVkO9MSJgEzoWkxONgaQpICWg8vAN74wlA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.7.0", - "@typescript-eslint/visitor-keys": "8.7.0" + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7230,14 +7647,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.7.0.tgz", - "integrity": "sha512-tl0N0Mj3hMSkEYhLkjREp54OSb/FI6qyCzfiiclvJvOqre6hsZTGSnHtmFLDU8TIM62G7ygEa1bI08lcuRwEnQ==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.1.tgz", + "integrity": "sha512-qSVnpcbLP8CALORf0za+vjLYj1Wp8HSoiI8zYU5tHxRVj30702Z1Yw4cLwfNKhTPWp5+P+k1pjmD5Zd1nhxiZA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.7.0", - "@typescript-eslint/utils": "8.7.0", + "@typescript-eslint/typescript-estree": "8.8.1", + "@typescript-eslint/utils": "8.8.1", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -7255,9 +7672,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.7.0.tgz", - "integrity": "sha512-LLt4BLHFwSfASHSF2K29SZ+ZCsbQOM+LuarPjRUuHm+Qd09hSe3GCeaQbcCr+Mik+0QFRmep/FyZBO6fJ64U3w==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.1.tgz", + "integrity": "sha512-WCcTP4SDXzMd23N27u66zTKMuEevH4uzU8C9jf0RO4E04yVHgQgW+r+TeVTNnO1KIfrL8ebgVVYYMMO3+jC55Q==", "dev": true, "license": "MIT", "engines": { @@ -7269,14 +7686,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.7.0.tgz", - "integrity": "sha512-MC8nmcGHsmfAKxwnluTQpNqceniT8SteVwd2voYlmiSWGOtjvGXdPl17dYu2797GVscK30Z04WRM28CrKS9WOg==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.1.tgz", + "integrity": "sha512-A5d1R9p+X+1js4JogdNilDuuq+EHZdsH9MjTVxXOdVFfTJXunKJR/v+fNNyO4TnoOn5HqobzfRlc70NC6HTcdg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.7.0", - "@typescript-eslint/visitor-keys": "8.7.0", + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -7298,16 +7715,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.7.0.tgz", - "integrity": "sha512-ZbdUdwsl2X/s3CiyAu3gOlfQzpbuG3nTWKPoIvAu1pu5r8viiJvv2NPN2AqArL35NCYtw/lrPPfM4gxrMLNLPw==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.1.tgz", + "integrity": "sha512-/QkNJDbV0bdL7H7d0/y0qBbV2HTtf0TIyjSDTvvmQEzeVx8jEImEbLuOA4EsvE8gIgqMitns0ifb5uQhMj8d9w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.7.0", - "@typescript-eslint/types": "8.7.0", - "@typescript-eslint/typescript-estree": "8.7.0" + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/typescript-estree": "8.8.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7321,13 +7738,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.7.0.tgz", - "integrity": "sha512-b1tx0orFCCh/THWPQa2ZwWzvOeyzzp36vkJYOpVg0u8UVOIsfVrnuC9FqAw9gRKn+rG2VmWQ/zDJZzkxUnj/XQ==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.1.tgz", + "integrity": "sha512-0/TdC3aeRAsW7MDvYRwEc1Uwm0TIBfzjPFgg60UU2Haj5qsCs9cc3zNgY71edqE3LbWfF/WoZQd3lJoDXFQpag==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.7.0", + "@typescript-eslint/types": "8.8.1", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -7835,6 +8252,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -7848,6 +8266,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -8347,6 +8766,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8691,9 +9111,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001664", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001664.tgz", - "integrity": "sha512-AmE7k4dXiNKQipgn7a2xg558IRqPN3jMQY/rOsbxDhrd0tyChwbITBfiwtnqz8bi2M5mIWbxAYBvk7W7QBUS2g==", + "version": "1.0.30001668", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz", + "integrity": "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==", "funding": [ { "type": "opencollective", @@ -8761,6 +9181,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -8781,6 +9202,19 @@ "fsevents": "~2.3.2" } }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -8851,9 +9285,9 @@ } }, "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", + "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", "dev": true, "license": "MIT", "engines": { @@ -9020,9 +9454,9 @@ } }, "node_modules/code-block-writer": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.2.tgz", - "integrity": "sha512-XfXzAGiStXSmCIwrkdfvc7FS5Dtj8yelCtyOf2p2skCAfvLd6zu0rGzuS9NSCO3bq1JKpFZ7tbKdKlcd5occQA==", + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", "dev": true, "license": "MIT" }, @@ -9218,9 +9652,9 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true, "license": "MIT", "engines": { @@ -9272,19 +9706,6 @@ "webpack": "^5.1.0" } }, - "node_modules/copy-webpack-plugin/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/core-js": { "version": "3.38.1", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.38.1.tgz", @@ -10090,19 +10511,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/default-gateway": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", - "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "execa": "^5.0.0" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -10416,9 +10824,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.29", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.29.tgz", - "integrity": "sha512-PF8n2AlIhCKXQ+gTpiJi0VhcHDb69kYX4MtCiivctc2QD3XuNZ/XIOlbGzt7WAjjEev0TtaH6Cu3arZExm5DOw==", + "version": "1.5.36", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.36.tgz", + "integrity": "sha512-HYTX8tKge/VNp6FGO+f/uVDmUkq+cEfcxYhKf15Akc4M5yxt5YmorwlAitKWjWhWQnKcDRBAQKXkhqqXMqcrjw==", "license": "ISC" }, "node_modules/emittery": { @@ -10699,9 +11107,9 @@ } }, "node_modules/eslint": { - "version": "9.11.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.11.1.tgz", - "integrity": "sha512-MobhYKIoAO1s1e4VUrgx1l1Sk2JBR/Gqjjgw8+mfgoLE2xwsHur4gdfTxyTgShrhvdVFTaJSgMiQBl1jv/AWxg==", + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.12.0.tgz", + "integrity": "sha512-UVIOlTEWxwIopRL1wgSQYdnVDcEvs2wyaO6DGo5mXqe3r16IoCNWkR29iHhyaP4cICWjbgbmFUGAhh0GJRuGZw==", "dev": true, "license": "MIT", "dependencies": { @@ -10710,11 +11118,11 @@ "@eslint/config-array": "^0.18.0", "@eslint/core": "^0.6.0", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.11.1", + "@eslint/js": "9.12.0", "@eslint/plugin-kit": "^0.2.0", + "@humanfs/node": "^0.16.5", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.3.0", - "@nodelib/fs.walk": "^1.2.8", + "@humanwhocodes/retry": "^0.3.1", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -10722,9 +11130,9 @@ "cross-spawn": "^7.0.2", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.0.2", - "eslint-visitor-keys": "^4.0.0", - "espree": "^10.1.0", + "eslint-scope": "^8.1.0", + "eslint-visitor-keys": "^4.1.0", + "espree": "^10.2.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -10734,13 +11142,11 @@ "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { @@ -11157,9 +11563,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", - "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", + "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -11281,9 +11687,9 @@ } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", "dev": true, "license": "Apache-2.0", "engines": { @@ -11293,19 +11699,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/eslint/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -11350,15 +11743,15 @@ } }, "node_modules/espree": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", - "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", + "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.0.0" + "eslint-visitor-keys": "^4.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -11368,9 +11761,9 @@ } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", "dev": true, "license": "Apache-2.0", "engines": { @@ -11658,6 +12051,18 @@ "node": ">=8.6.0" } }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-patch": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", @@ -11939,9 +12344,9 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", "dev": true, "license": "MIT", "dependencies": { @@ -12048,31 +12453,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/fs-extra/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/fs-minipass": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", @@ -12097,6 +12477,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -12292,15 +12673,16 @@ } }, "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "license": "ISC", "dependencies": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" }, "engines": { - "node": ">= 6" + "node": ">=10.13.0" } }, "node_modules/glob-to-regexp": { @@ -13053,6 +13435,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -13201,16 +13584,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-plain-obj": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", @@ -14254,9 +14627,9 @@ } }, "node_modules/jest-fail-on-console": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/jest-fail-on-console/-/jest-fail-on-console-3.3.0.tgz", - "integrity": "sha512-J9rnFQvQwkcGJw01zCEKe2Uag+E926lFgIyaQGep2LqhQH7OCRHyD+tm/jnNoKlSRnOBO60DmzMjeQAVI3f5cw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jest-fail-on-console/-/jest-fail-on-console-3.3.1.tgz", + "integrity": "sha512-dmq/dmh5OBgJlD1MJdpznzwFQP8S7msf3ghTGWQLGhagWwHKzGtqXza76nuJUKOK7BdwqcTK6CCE49Xxv4ckUQ==", "dev": true, "license": "MIT" }, @@ -15481,13 +15854,13 @@ "license": "MIT" }, "node_modules/jsdom": { - "version": "25.0.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.0.tgz", - "integrity": "sha512-OhoFVT59T7aEq75TVw9xxEfkXgacpqAhQaYgP9y/fDqWQCMB/b1H66RfmPm/MaeaAIU9nDwMOVTlPN51+ao6CQ==", + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", "dev": true, "license": "MIT", "dependencies": { - "cssstyle": "^4.0.1", + "cssstyle": "^4.1.0", "data-urls": "^5.0.0", "decimal.js": "^10.4.3", "form-data": "^4.0.0", @@ -15500,7 +15873,7 @@ "rrweb-cssom": "^0.7.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.4", + "tough-cookie": "^5.0.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", @@ -15583,29 +15956,6 @@ "dev": true, "license": "MIT" }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonfile/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -15849,11 +16199,14 @@ } }, "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.3.tgz", + "integrity": "sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } }, "node_modules/lint-staged": { "version": "15.2.10", @@ -16534,9 +16887,9 @@ } }, "node_modules/memfs": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.12.0.tgz", - "integrity": "sha512-74wDsex5tQDSClVkeK1vtxqYCAgCoXxx+K4NSHzgU/muYVYByFqa+0RnrPO9NM6naWm1+G9JmZ0p6QHhXmeYfA==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.13.0.tgz", + "integrity": "sha512-dIs5KGy24fbdDhIAg0RxXpFqQp3RwL6wgSMRF9OSuphL/Uc9a4u2/SDJKPLj/zUgtOGKuHrRMrj563+IErj4Cg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -16934,9 +17287,9 @@ "license": "MIT" }, "node_modules/monaco-editor": { - "version": "0.51.0", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.51.0.tgz", - "integrity": "sha512-xaGwVV1fq343cM7aOYB6lVE4Ugf0UyimdD/x5PWcWBMKENwectaEu77FAN7c5sFiyumqeJdX1RPTh1ocioyDjw==", + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz", + "integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==", "license": "MIT" }, "node_modules/moo-color": { @@ -17040,9 +17393,9 @@ } }, "node_modules/nan": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", - "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", "license": "MIT", "optional": true }, @@ -17432,6 +17785,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -17589,22 +17943,22 @@ } }, "node_modules/nwsapi": { - "version": "2.2.12", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.12.tgz", - "integrity": "sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==", + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.13.tgz", + "integrity": "sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==", "dev": true, "license": "MIT" }, "node_modules/nx": { - "version": "19.6.5", - "resolved": "https://registry.npmjs.org/nx/-/nx-19.6.5.tgz", - "integrity": "sha512-igPYPsBF1BM1YxEiGDvaLOz0CWWoEvxzR7yQg3iULjGG9zKgDFNHHIHJwkyHsCBTtMhhkgeUl16PsTVgDuil3A==", + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/nx/-/nx-19.8.4.tgz", + "integrity": "sha512-fc833c3UKo6kuoG4z0kSKet17yWym3VzcQ+yPWYspxxxd8GFVVk42+9wieyVQDi9YqtKZQ6PdQfSEPm59/M7SA==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { "@napi-rs/wasm-runtime": "0.2.4", - "@nrwl/tao": "19.6.5", + "@nrwl/tao": "19.8.4", "@yarnpkg/lockfile": "^1.1.0", "@yarnpkg/parsers": "3.0.0-rc.46", "@zkochan/js-yaml": "0.0.7", @@ -17619,11 +17973,10 @@ "figures": "3.2.0", "flat": "^5.0.2", "front-matter": "^4.0.2", - "fs-extra": "^11.1.0", "ignore": "^5.0.4", "jest-diff": "^29.4.1", "jsonc-parser": "3.2.0", - "lines-and-columns": "~2.0.3", + "lines-and-columns": "2.0.3", "minimatch": "9.0.3", "node-machine-id": "1.1.12", "npm-run-path": "^4.0.1", @@ -17644,16 +17997,16 @@ "nx-cloud": "bin/nx-cloud.js" }, "optionalDependencies": { - "@nx/nx-darwin-arm64": "19.6.5", - "@nx/nx-darwin-x64": "19.6.5", - "@nx/nx-freebsd-x64": "19.6.5", - "@nx/nx-linux-arm-gnueabihf": "19.6.5", - "@nx/nx-linux-arm64-gnu": "19.6.5", - "@nx/nx-linux-arm64-musl": "19.6.5", - "@nx/nx-linux-x64-gnu": "19.6.5", - "@nx/nx-linux-x64-musl": "19.6.5", - "@nx/nx-win32-arm64-msvc": "19.6.5", - "@nx/nx-win32-x64-msvc": "19.6.5" + "@nx/nx-darwin-arm64": "19.8.4", + "@nx/nx-darwin-x64": "19.8.4", + "@nx/nx-freebsd-x64": "19.8.4", + "@nx/nx-linux-arm-gnueabihf": "19.8.4", + "@nx/nx-linux-arm64-gnu": "19.8.4", + "@nx/nx-linux-arm64-musl": "19.8.4", + "@nx/nx-linux-x64-gnu": "19.8.4", + "@nx/nx-linux-x64-musl": "19.8.4", + "@nx/nx-win32-arm64-msvc": "19.8.4", + "@nx/nx-win32-x64-msvc": "19.8.4" }, "peerDependencies": { "@swc-node/register": "^1.8.0", @@ -17714,19 +18067,6 @@ "node": ">=8" } }, - "node_modules/nx/node_modules/cli-spinners": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", - "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/nx/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -17820,16 +18160,6 @@ "dev": true, "license": "MIT" }, - "node_modules/nx/node_modules/lines-and-columns": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.4.tgz", - "integrity": "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, "node_modules/nx/node_modules/minimatch": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", @@ -18388,6 +18718,13 @@ "dev": true, "license": "MIT" }, + "node_modules/parse-json/node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, "node_modules/parse-node-version": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", @@ -18548,9 +18885,9 @@ } }, "node_modules/pdfjs-dist": { - "version": "4.6.82", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.6.82.tgz", - "integrity": "sha512-BUOryeRFwvbLe0lOU6NhkJNuVQUp06WxlJVVCsxdmJ4y5cU3O3s3/0DunVdK1PMm7v2MUw52qKYaidhDH1Z9+w==", + "version": "4.7.76", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.7.76.tgz", + "integrity": "sha512-8y6wUgC/Em35IumlGjaJOCm3wV4aY/6sqnIT3fVW/67mXsOZ9HWBn8GDKmJUK0GSzpbmX3gQqwfoFayp78Mtqw==", "license": "Apache-2.0", "engines": { "node": ">=18" @@ -18885,9 +19222,9 @@ "license": "MIT" }, "node_modules/posthog-js": { - "version": "1.165.0", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.165.0.tgz", - "integrity": "sha512-rUfRJobvOz3Q9Er+zwb32Eq2qs+ToBe/B4k4IoKzmyszI7240Rf4xVWRB0ky8LvmdZfCeYX5knS2Uv3pnn/d5A==", + "version": "1.167.0", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.167.0.tgz", + "integrity": "sha512-/zXQ6tuJgiF1d4mgg3UsAi/uoyg7UnfFNQtikuALmaE53xFExpcAKbMfHPG/f54QgTvLxSHyGL1kFl/1uspkGg==", "license": "MIT", "dependencies": { "fflate": "^0.4.8", @@ -18896,9 +19233,9 @@ } }, "node_modules/preact": { - "version": "10.24.1", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.1.tgz", - "integrity": "sha512-PnBAwFI3Yjxxcxw75n6VId/5TFxNW/81zexzWD9jn1+eSrOP84NdsS38H5IkF/UH3frqRPT+MvuCoVHjTDTnDw==", + "version": "10.24.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.2.tgz", + "integrity": "sha512-1cSoF0aCC8uaARATfrlz4VCBqE8LwZwRfLgkxJOQwAlQt6ayTmi0D9OF7nXid1POI5SZidFuG9CnlXbDfLqY/Q==", "license": "MIT", "funding": { "type": "opencollective", @@ -19336,6 +19673,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -19348,6 +19686,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -19439,16 +19778,16 @@ "license": "MIT" }, "node_modules/regexpu-core": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", - "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.1.1.tgz", + "integrity": "sha512-k67Nb9jvwJcJmVpw0jPttR1/zVfnKf8Km0IPatrU/zJ5XeG3+Slx0xLXs9HByJSzXzrlz5EDvN6yLNMDc2qdnw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/regjsgen": "^0.8.0", "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.11.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.1.0" }, @@ -19456,26 +19795,37 @@ "node": ">=4" } }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, "node_modules/regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.11.1.tgz", + "integrity": "sha512-1DHODs4B8p/mQHU9kr+jv8+wIC9mtG4eBHxWxIq5mhjE3D5oORhCc6deRKzTjs9DcfRFmj9BHSDguZklqCGFWQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "jsesc": "~0.5.0" + "jsesc": "~3.0.2" }, "bin": { "regjsparser": "bin/parser" } }, "node_modules/regjsparser/node_modules/jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", "dev": true, + "license": "MIT", "bin": { "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" } }, "node_modules/require-directory": { @@ -19877,12 +20227,13 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.79.3", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.3.tgz", - "integrity": "sha512-m7dZxh0W9EZ3cw50Me5GOuYm/tVAJAn91SUnohLRo9cXBixGUOdvmryN+dXpwR831bhoY3Zv7rEFt85PUwTmzA==", + "version": "1.79.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.5.tgz", + "integrity": "sha512-W1h5kp6bdhqFh2tk3DsI771MoEJjvrSY/2ihJRJS4pjIyfJCw0nTsxqhnrUzaLMOJjFchj8rOvraI/YUVjtx5g==", "dev": true, "license": "MIT", "dependencies": { + "@parcel/watcher": "^2.4.1", "chokidar": "^4.0.0", "immutable": "^4.0.0", "source-map-js": ">=0.6.2 <2.0.0" @@ -19952,9 +20303,9 @@ } }, "node_modules/sass/node_modules/readdirp": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.1.tgz", - "integrity": "sha512-GkMg9uOTpIWWKbSsgwb5fA4EavTR+SG/PMPoAY8hkhHfEEY0/vqljY+XHqtDf2cr2IJtoNRDbrrEpZUiZCkYRw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", "dev": true, "license": "MIT", "engines": { @@ -21058,9 +21409,9 @@ "license": "MIT" }, "node_modules/synckit": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", - "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", + "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", "dev": true, "license": "MIT", "dependencies": { @@ -21412,22 +21763,22 @@ "license": "MIT" }, "node_modules/tldts": { - "version": "6.1.47", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.47.tgz", - "integrity": "sha512-R/K2tZ5MiY+mVrnSkNJkwqYT2vUv1lcT6wJvd2emGaMJ7PHUGRY4e3tUsdFCXgqxi2QgbHjL3yJgXCo40v9Hxw==", + "version": "6.1.51", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.51.tgz", + "integrity": "sha512-33lfQoL0JsDogIbZ8fgRyvv77GnRtwkNE/MOKocwUgPO1WrSfsq7+vQRKxRQZai5zd+zg97Iv9fpFQSzHyWdLA==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^6.1.47" + "tldts-core": "^6.1.51" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.47", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.47.tgz", - "integrity": "sha512-6SWyFMnlst1fEt7GQVAAu16EGgFK0cLouH/2Mk6Ftlwhv3Ol40L0dlpGMcnnNiiOMyD2EV/aF3S+U2nKvvLvrA==", + "version": "6.1.51", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.51.tgz", + "integrity": "sha512-bu9oCYYWC1iRjx+3UnAjqCsfrWNZV1ghNQf49b3w5xE8J/tNShHTzp5syWJfwGH+pxUgTTLUnzHnfuydW7wmbg==", "dev": true, "license": "MIT" }, @@ -21945,9 +22296,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "funding": [ { "type": "opencollective", @@ -21964,8 +22315,8 @@ ], "license": "MIT", "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" @@ -22797,9 +23148,9 @@ } }, "node_modules/webpack-dev-server": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.0.4.tgz", - "integrity": "sha512-dljXhUgx3HqKP2d8J/fUMvhxGhzjeNVarDLcbO/EWMSgRizDkxHQDZQaLFL5VJY9tRBj2Gz+rvCEYYvhbqPHNA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.1.0.tgz", + "integrity": "sha512-aQpaN81X6tXie1FoOB7xlMfCsN19pSvRAeYUHOdFWOlhpQ/LlbfTqYwwmEDFV0h8GGuqmCmKmT+pxcUV/Nt2gQ==", "dev": true, "license": "MIT", "dependencies": { @@ -22816,8 +23167,7 @@ "colorette": "^2.0.10", "compression": "^1.7.4", "connect-history-api-fallback": "^2.0.0", - "default-gateway": "^6.0.3", - "express": "^4.17.3", + "express": "^4.19.2", "graceful-fs": "^4.2.6", "html-entities": "^2.4.0", "http-proxy-middleware": "^2.0.3", @@ -22825,14 +23175,13 @@ "launch-editor": "^2.6.1", "open": "^10.0.3", "p-retry": "^6.2.0", - "rimraf": "^5.0.5", "schema-utils": "^4.2.0", "selfsigned": "^2.4.1", "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", - "webpack-dev-middleware": "^7.1.0", - "ws": "^8.16.0" + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" }, "bin": { "webpack-dev-server": "bin/webpack-dev-server.js" @@ -22857,9 +23206,9 @@ } }, "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", + "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 03ab87fb7b80..755adfae3d3e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "artemis", - "version": "7.5.6", + "version": "7.6.0", "description": "Interactive Learning with Individual Feedback", "private": true, "license": "MIT", @@ -13,18 +13,18 @@ "node_modules" ], "dependencies": { - "@angular/animations": "18.2.6", - "@angular/cdk": "18.2.6", - "@angular/common": "18.2.6", - "@angular/compiler": "18.2.6", - "@angular/core": "18.2.6", - "@angular/forms": "18.2.6", - "@angular/localize": "18.2.6", - "@angular/material": "18.2.6", - "@angular/platform-browser": "18.2.6", - "@angular/platform-browser-dynamic": "18.2.6", - "@angular/router": "18.2.6", - "@angular/service-worker": "18.2.6", + "@angular/animations": "18.2.8", + "@angular/cdk": "18.2.8", + "@angular/common": "18.2.8", + "@angular/compiler": "18.2.8", + "@angular/core": "18.2.8", + "@angular/forms": "18.2.8", + "@angular/localize": "18.2.8", + "@angular/material": "18.2.8", + "@angular/platform-browser": "18.2.8", + "@angular/platform-browser-dynamic": "18.2.8", + "@angular/router": "18.2.8", + "@angular/service-worker": "18.2.8", "@ctrl/ngx-emoji-mart": "9.2.0", "@danielmoncada/angular-datetime-picker": "18.1.0", "@fingerprintjs/fingerprintjs": "4.5.0", @@ -36,7 +36,7 @@ "@ng-bootstrap/ng-bootstrap": "17.0.1", "@ngx-translate/core": "15.0.0", "@ngx-translate/http-loader": "8.0.0", - "@sentry/angular": "8.32.0", + "@sentry/angular": "8.34.0", "@siemens/ngx-datatable": "22.4.1", "@swimlane/ngx-charts": "20.5.0", "@swimlane/ngx-graph": "8.4.0", @@ -58,12 +58,12 @@ "jszip": "3.10.1", "lodash-es": "4.17.21", "mobile-drag-drop": "3.0.0-rc.0", - "monaco-editor": "0.51.0", + "monaco-editor": "0.52.0", "ngx-infinite-scroll": "18.0.0", "ngx-webstorage": "18.0.0", "papaparse": "5.4.1", - "pdfjs-dist": "4.6.82", - "posthog-js": "1.165.0", + "pdfjs-dist": "4.7.76", + "posthog-js": "1.167.0", "rxjs": "7.8.1", "showdown": "2.1.0", "showdown-highlight": "3.1.0", @@ -88,18 +88,20 @@ "d3-transition": "^3.0.1" }, "@typescript-eslint/utils": { - "eslint": "^9.11.0" + "eslint": "^9.12.0" }, "braces": "3.0.3", + "cookie": "0.7.1", "critters": "0.0.24", "debug": "4.3.7", "eslint-plugin-deprecation": { - "eslint": "^9.11.0" + "eslint": "^9.12.0" }, "eslint-plugin-jest": { - "@typescript-eslint/eslint-plugin": "^8.6.0" + "@typescript-eslint/eslint-plugin": "^8.8.0" }, - "jsdom": "25.0.0", + "express": "4.21.0", + "jsdom": "25.0.1", "katex": "0.16.11", "postcss": "8.4.47", "rimraf": "6.0.1", @@ -110,36 +112,37 @@ "tough-cookie": "5.0.0", "vite": "5.4.8", "webpack-dev-middleware": "7.4.2", + "webpack-dev-server": "5.1.0", "word-wrap": "1.2.5", "ws": "8.18.0", "yargs-parser": "21.1.1" }, "devDependencies": { "@angular-builders/jest": "18.0.0", - "@angular-devkit/build-angular": "18.2.6", + "@angular-devkit/build-angular": "18.2.8", "@angular-eslint/builder": "18.3.1", "@angular-eslint/eslint-plugin": "18.3.1", "@angular-eslint/eslint-plugin-template": "18.3.1", "@angular-eslint/schematics": "18.3.1", "@angular-eslint/template-parser": "18.3.1", - "@angular/cli": "18.2.6", - "@angular/compiler-cli": "18.2.6", - "@angular/language-service": "18.2.6", - "@sentry/types": "8.32.0", + "@angular/cli": "18.2.8", + "@angular/compiler-cli": "18.2.8", + "@angular/language-service": "18.2.8", + "@sentry/types": "8.34.0", "@types/crypto-js": "4.2.2", "@types/d3-shape": "3.1.6", "@types/dompurify": "3.0.5", "@types/jest": "29.5.13", "@types/lodash-es": "4.17.12", - "@types/node": "22.7.3", + "@types/node": "22.7.5", "@types/papaparse": "5.3.14", "@types/showdown": "2.0.6", "@types/smoothscroll-polyfill": "0.3.4", "@types/sockjs-client": "1.5.4", "@types/uuid": "10.0.0", - "@typescript-eslint/eslint-plugin": "8.7.0", - "@typescript-eslint/parser": "8.7.0", - "eslint": "9.11.1", + "@typescript-eslint/eslint-plugin": "8.8.1", + "@typescript-eslint/parser": "8.8.1", + "eslint": "9.12.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-deprecation": "3.0.0", "eslint-plugin-jest": "28.8.3", @@ -151,7 +154,7 @@ "jest-canvas-mock": "2.5.2", "jest-date-mock": "1.0.10", "jest-extended": "4.0.2", - "jest-fail-on-console": "3.3.0", + "jest-fail-on-console": "3.3.1", "jest-junit": "16.0.0", "jest-preset-angular": "14.2.4", "lint-staged": "15.2.10", @@ -159,7 +162,7 @@ "ng-mocks": "14.13.1", "prettier": "3.3.3", "rimraf": "6.0.1", - "sass": "1.79.3", + "sass": "1.79.5", "ts-jest": "29.2.5", "typescript": "5.5.4", "weak-napi": "2.0.2" diff --git a/prebuild.mjs b/prebuild.mjs index 7ef783be432c..5f362babe3fc 100644 --- a/prebuild.mjs +++ b/prebuild.mjs @@ -5,10 +5,11 @@ * - webpack.DefinePlugin and * - MergeJsonWebpackPlugin */ -import fs from "fs"; -import path from "path"; -import { hashElement } from "folder-hash"; -import { fileURLToPath } from "url"; +import fs from 'fs'; +import path from 'path'; +import { hashElement } from 'folder-hash'; +import { fileURLToPath } from 'url'; +import * as esbuild from 'esbuild'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -111,4 +112,25 @@ for (const group of groups) { } } +/* + * The workers of the monaco editor must be bundled separately. + * Specialized workers are available in the vs/esm/language/ directory. + * Be sure to modify the MonacoConfig if you choose to add a worker here. + * For more details, refer to https://github.com/microsoft/monaco-editor/blob/main/samples/browser-esm-esbuild/build.js + */ +const workerEntryPoints = [ + 'vs/language/json/json.worker.js', + 'vs/language/css/css.worker.js', + 'vs/language/html/html.worker.js', + 'vs/language/typescript/ts.worker.js', + 'vs/editor/editor.worker.js' +]; +await esbuild.build({ + entryPoints: workerEntryPoints.map((entry) => `node_modules/monaco-editor/esm/${entry}`), + bundle: true, + format: 'esm', + outbase: 'node_modules/monaco-editor/esm', + outdir: 'node_modules/monaco-editor/bundles' +}); + console.log("Pre-Build complete!"); diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/domain/Result.java b/src/main/java/de/tum/cit/aet/artemis/assessment/domain/Result.java index cc14d7a35e34..77c01c6fae19 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/domain/Result.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/domain/Result.java @@ -629,7 +629,7 @@ public boolean isAutomatic() { * @return true if the result is an automatic AI Athena result */ @JsonIgnore - public boolean isAthenaAutomatic() { + public boolean isAthenaBased() { return AssessmentType.AUTOMATIC_ATHENA == assessmentType; } diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/repository/ResultRepository.java b/src/main/java/de/tum/cit/aet/artemis/assessment/repository/ResultRepository.java index 87c381b777d9..5a7b75e82de5 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/repository/ResultRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/repository/ResultRepository.java @@ -427,6 +427,14 @@ SELECT COUNT(DISTINCT p) */ boolean existsByParticipationId(long participationId); + /** + * Checks if a result exists for the given submission ID. + * + * @param submissionId the ID of the submission to check. + * @return true if a result exists for the given submission ID, false otherwise. + */ + boolean existsBySubmissionId(long submissionId); + /** * Returns true if there is at least one result for the given exercise. * diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/domain/competency/LearningPath.java b/src/main/java/de/tum/cit/aet/artemis/atlas/domain/competency/LearningPath.java index 267efe722982..ab4c961be143 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/domain/competency/LearningPath.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/domain/competency/LearningPath.java @@ -31,6 +31,12 @@ public class LearningPath extends DomainObject { @Column(name = "progress") private int progress; + /** + * flag indicating if a student started the learning path + */ + @Column(name = "started_by_student") + private boolean startedByStudent = false; + @ManyToOne @JoinColumn(name = "user_id") private User user; @@ -89,8 +95,16 @@ public void removeCompetency(CourseCompetency competency) { this.competencies.remove(competency); } + public boolean isStartedByStudent() { + return startedByStudent; + } + + public void setStartedByStudent(boolean startedByStudent) { + this.startedByStudent = startedByStudent; + } + @Override public String toString() { - return "LearningPath{" + "id=" + getId() + ", user=" + user + ", course=" + course + ", competencies=" + competencies + '}'; + return "LearningPath{" + "id=" + getId() + ", user=" + user + ", course=" + course + ", competencies=" + competencies + ", startedByStudent=" + startedByStudent + "}"; } } diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/dto/CompetencyImportOptionsDTO.java b/src/main/java/de/tum/cit/aet/artemis/atlas/dto/CompetencyImportOptionsDTO.java new file mode 100644 index 000000000000..7d64a43ca560 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/dto/CompetencyImportOptionsDTO.java @@ -0,0 +1,12 @@ +package de.tum.cit.aet.artemis.atlas.dto; + +import java.time.ZonedDateTime; +import java.util.Optional; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record CompetencyImportOptionsDTO(Set competencyIds, Optional sourceCourseId, boolean importRelations, boolean importExercises, boolean importLectures, + Optional referenceDate, boolean isReleaseDate) { +} diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/dto/LearningPathDTO.java b/src/main/java/de/tum/cit/aet/artemis/atlas/dto/LearningPathDTO.java new file mode 100644 index 000000000000..f61598b30cf4 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/dto/LearningPathDTO.java @@ -0,0 +1,13 @@ +package de.tum.cit.aet.artemis.atlas.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.cit.aet.artemis.atlas.domain.competency.LearningPath; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record LearningPathDTO(long id, boolean startedByStudent, int progress) { + + public static LearningPathDTO of(LearningPath learningPath) { + return new LearningPathDTO(learningPath.getId(), learningPath.isStartedByStudent(), learningPath.getProgress()); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/dto/metrics/CompetencyInformationDTO.java b/src/main/java/de/tum/cit/aet/artemis/atlas/dto/metrics/CompetencyInformationDTO.java index 02e0fc7edeb2..f361bc402718 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/dto/metrics/CompetencyInformationDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/dto/metrics/CompetencyInformationDTO.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; +import de.tum.cit.aet.artemis.atlas.domain.competency.Competency; import de.tum.cit.aet.artemis.atlas.domain.competency.CompetencyTaxonomy; /** @@ -19,4 +20,15 @@ */ @JsonInclude(JsonInclude.Include.NON_EMPTY) public record CompetencyInformationDTO(long id, String title, String description, CompetencyTaxonomy taxonomy, ZonedDateTime softDueDate, boolean optional, int masteryThreshold) { + + /** + * Creates a CompetencyInformationDTO from a Competency. + * + * @param competency the Competency to create the DTO from + * @return the created DTO + */ + public static CompetencyInformationDTO of(C competency) { + return new CompetencyInformationDTO(competency.getId(), competency.getTitle(), competency.getDescription(), competency.getTaxonomy(), competency.getSoftDueDate(), + competency.isOptional(), competency.getMasteryThreshold()); + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/dto/metrics/LectureUnitInformationDTO.java b/src/main/java/de/tum/cit/aet/artemis/atlas/dto/metrics/LectureUnitInformationDTO.java index 3a5d9cc8bcb4..8f4016a5f346 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/dto/metrics/LectureUnitInformationDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/dto/metrics/LectureUnitInformationDTO.java @@ -18,4 +18,15 @@ */ @JsonInclude(JsonInclude.Include.NON_EMPTY) public record LectureUnitInformationDTO(long id, long lectureId, String lectureTitle, String name, ZonedDateTime releaseDate, Class type) { + + /** + * Creates a LectureUnitInformationDTO from a LectureUnit. + * + * @param lectureUnit the LectureUnit to create the DTO from + * @return the created DTO + */ + public static LectureUnitInformationDTO of(L lectureUnit) { + return new LectureUnitInformationDTO(lectureUnit.getId(), lectureUnit.getLecture().getId(), lectureUnit.getLecture().getTitle(), lectureUnit.getName(), + lectureUnit.getReleaseDate(), lectureUnit.getClass()); + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/repository/CompetencyRepository.java b/src/main/java/de/tum/cit/aet/artemis/atlas/repository/CompetencyRepository.java index eb2fc3eccab5..91ae85978b42 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/repository/CompetencyRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/repository/CompetencyRepository.java @@ -26,9 +26,13 @@ public interface CompetencyRepository extends ArtemisJpaRepository findAllForCourse(@Param("courseId") long courseId); + Set findAllForCourseWithExercisesAndLectureUnitsAndLecturesAndAttachments(@Param("courseId") long courseId); @Query(""" SELECT c diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/repository/CourseCompetencyRepository.java b/src/main/java/de/tum/cit/aet/artemis/atlas/repository/CourseCompetencyRepository.java index 9a164343bc0e..d8b66519355c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/repository/CourseCompetencyRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/repository/CourseCompetencyRepository.java @@ -45,6 +45,44 @@ public interface CourseCompetencyRepository extends ArtemisJpaRepository findAllForCourse(@Param("courseId") long courseId); + @Query(""" + SELECT c + FROM CourseCompetency c + LEFT JOIN FETCH c.exercises ex + LEFT JOIN FETCH c.lectureUnits lu + LEFT JOIN FETCH lu.lecture l + LEFT JOIN FETCH l.attachments + WHERE c.course.id = :courseId + """) + Set findAllForCourseWithExercisesAndLectureUnitsAndLecturesAndAttachments(@Param("courseId") long courseId); + + @Query(""" + SELECT c + FROM CourseCompetency c + LEFT JOIN FETCH c.exercises ex + LEFT JOIN FETCH c.lectureUnits lu + LEFT JOIN FETCH lu.lecture l + LEFT JOIN FETCH l.lectureUnits + LEFT JOIN FETCH l.attachments + WHERE c.id = :id + """) + Optional findByIdWithExercisesAndLectureUnitsAndLectures(@Param("id") long id); + + default CourseCompetency findByIdWithExercisesAndLectureUnitsAndLecturesElseThrow(long id) { + return getValueElseThrow(findByIdWithExercisesAndLectureUnitsAndLectures(id), id); + } + + @Query(""" + SELECT c + FROM CourseCompetency c + LEFT JOIN FETCH c.exercises ex + LEFT JOIN FETCH c.lectureUnits lu + LEFT JOIN FETCH lu.lecture l + LEFT JOIN FETCH l.attachments + WHERE c.id IN :ids + """) + Set findAllByIdWithExercisesAndLectureUnitsAndLecturesAndAttachments(@Param("ids") Set ids); + /** * Fetches all information related to the calculation of the mastery for exercises in a competency. * The complex grouping by is necessary for postgres diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/repository/PrerequisiteRepository.java b/src/main/java/de/tum/cit/aet/artemis/atlas/repository/PrerequisiteRepository.java index a2187579a427..9616c2a5f34b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/repository/PrerequisiteRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/repository/PrerequisiteRepository.java @@ -19,26 +19,30 @@ public interface PrerequisiteRepository extends ArtemisJpaRepository findAllByCourseIdOrderById(long courseId); @Query(""" - SELECT c - FROM Prerequisite c - WHERE c.course.id = :courseId + SELECT p + FROM Prerequisite p + LEFT JOIN FETCH p.exercises + LEFT JOIN FETCH p.lectureUnits lu + LEFT JOIN FETCH lu.lecture l + LEFT JOIN FETCH l.attachments + WHERE p.course.id = :courseId """) - Set findAllForCourse(@Param("courseId") long courseId); + Set findAllForCourseWithExercisesAndLectureUnitsAndLecturesAndAttachments(@Param("courseId") long courseId); @Query(""" - SELECT c - FROM Prerequisite c - LEFT JOIN FETCH c.lectureUnits lu - LEFT JOIN FETCH c.exercises - WHERE c.id = :competencyId + SELECT p + FROM Prerequisite p + LEFT JOIN FETCH p.lectureUnits lu + LEFT JOIN FETCH p.exercises + WHERE p.id = :competencyId """) Optional findByIdWithLectureUnitsAndExercises(@Param("competencyId") long competencyId); @Query(""" - SELECT c - FROM Prerequisite c - LEFT JOIN FETCH c.lectureUnits lu - WHERE c.id = :competencyId + SELECT p + FROM Prerequisite p + LEFT JOIN FETCH p.lectureUnits lu + WHERE p.id = :competencyId """) Optional findByIdWithLectureUnits(@Param("competencyId") long competencyId); diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/service/LearningObjectImportService.java b/src/main/java/de/tum/cit/aet/artemis/atlas/service/LearningObjectImportService.java new file mode 100644 index 000000000000..6b67d8f01b44 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/service/LearningObjectImportService.java @@ -0,0 +1,445 @@ +package de.tum.cit.aet.artemis.atlas.service; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + +import java.io.IOException; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.springframework.util.function.ThrowingBiFunction; + +import com.fasterxml.jackson.core.JsonProcessingException; + +import de.tum.cit.aet.artemis.assessment.domain.GradingCriterion; +import de.tum.cit.aet.artemis.assessment.repository.GradingCriterionRepository; +import de.tum.cit.aet.artemis.atlas.domain.competency.CourseCompetency; +import de.tum.cit.aet.artemis.atlas.dto.CompetencyImportOptionsDTO; +import de.tum.cit.aet.artemis.atlas.dto.CompetencyWithTailRelationDTO; +import de.tum.cit.aet.artemis.atlas.repository.CourseCompetencyRepository; +import de.tum.cit.aet.artemis.core.domain.Course; +import de.tum.cit.aet.artemis.core.exception.NoUniqueQueryException; +import de.tum.cit.aet.artemis.exercise.domain.Exercise; +import de.tum.cit.aet.artemis.exercise.repository.ExerciseRepository; +import de.tum.cit.aet.artemis.fileupload.domain.FileUploadExercise; +import de.tum.cit.aet.artemis.fileupload.repository.FileUploadExerciseRepository; +import de.tum.cit.aet.artemis.fileupload.service.FileUploadExerciseImportService; +import de.tum.cit.aet.artemis.lecture.domain.Lecture; +import de.tum.cit.aet.artemis.lecture.domain.LectureUnit; +import de.tum.cit.aet.artemis.lecture.repository.LectureRepository; +import de.tum.cit.aet.artemis.lecture.repository.LectureUnitRepository; +import de.tum.cit.aet.artemis.lecture.service.LectureImportService; +import de.tum.cit.aet.artemis.lecture.service.LectureUnitImportService; +import de.tum.cit.aet.artemis.modeling.domain.ModelingExercise; +import de.tum.cit.aet.artemis.modeling.repository.ModelingExerciseRepository; +import de.tum.cit.aet.artemis.modeling.service.ModelingExerciseImportService; +import de.tum.cit.aet.artemis.plagiarism.service.PlagiarismDetectionConfigHelper; +import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; +import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseRepository; +import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseTaskRepository; +import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseImportService; +import de.tum.cit.aet.artemis.quiz.domain.QuizExercise; +import de.tum.cit.aet.artemis.quiz.repository.QuizExerciseRepository; +import de.tum.cit.aet.artemis.quiz.service.QuizExerciseImportService; +import de.tum.cit.aet.artemis.text.domain.TextExercise; +import de.tum.cit.aet.artemis.text.repository.TextExerciseRepository; +import de.tum.cit.aet.artemis.text.service.TextExerciseImportService; + +/** + * Service for importing learning objects related to competencies. + */ +@Profile(PROFILE_CORE) +@Service +public class LearningObjectImportService { + + private static final Logger log = LoggerFactory.getLogger(LearningObjectImportService.class); + + private final ExerciseRepository exerciseRepository; + + private final ProgrammingExerciseRepository programmingExerciseRepository; + + private final ProgrammingExerciseImportService programmingExerciseImportService; + + private final FileUploadExerciseRepository fileUploadExerciseRepository; + + private final FileUploadExerciseImportService fileUploadExerciseImportService; + + private final ModelingExerciseRepository modelingExerciseRepository; + + private final ModelingExerciseImportService modelingExerciseImportService; + + private final TextExerciseRepository textExerciseRepository; + + private final TextExerciseImportService textExerciseImportService; + + private final QuizExerciseRepository quizExerciseRepository; + + private final QuizExerciseImportService quizExerciseImportService; + + private final LectureRepository lectureRepository; + + private final LectureImportService lectureImportService; + + private final LectureUnitRepository lectureUnitRepository; + + private final LectureUnitImportService lectureUnitImportService; + + private final CourseCompetencyRepository courseCompetencyRepository; + + private final ProgrammingExerciseTaskRepository programmingExerciseTaskRepository; + + private final GradingCriterionRepository gradingCriterionRepository; + + public LearningObjectImportService(ExerciseRepository exerciseRepository, ProgrammingExerciseRepository programmingExerciseRepository, + ProgrammingExerciseImportService programmingExerciseImportService, FileUploadExerciseRepository fileUploadExerciseRepository, + FileUploadExerciseImportService fileUploadExerciseImportService, ModelingExerciseRepository modelingExerciseRepository, + ModelingExerciseImportService modelingExerciseImportService, TextExerciseRepository textExerciseRepository, TextExerciseImportService textExerciseImportService, + QuizExerciseRepository quizExerciseRepository, QuizExerciseImportService quizExerciseImportService, LectureRepository lectureRepository, + LectureImportService lectureImportService, LectureUnitRepository lectureUnitRepository, LectureUnitImportService lectureUnitImportService, + CourseCompetencyRepository courseCompetencyRepository, ProgrammingExerciseTaskRepository programmingExerciseTaskRepository, + GradingCriterionRepository gradingCriterionRepository) { + this.exerciseRepository = exerciseRepository; + this.programmingExerciseRepository = programmingExerciseRepository; + this.programmingExerciseImportService = programmingExerciseImportService; + this.fileUploadExerciseRepository = fileUploadExerciseRepository; + this.fileUploadExerciseImportService = fileUploadExerciseImportService; + this.modelingExerciseRepository = modelingExerciseRepository; + this.modelingExerciseImportService = modelingExerciseImportService; + this.textExerciseRepository = textExerciseRepository; + this.textExerciseImportService = textExerciseImportService; + this.quizExerciseRepository = quizExerciseRepository; + this.quizExerciseImportService = quizExerciseImportService; + this.lectureRepository = lectureRepository; + this.lectureImportService = lectureImportService; + this.lectureUnitRepository = lectureUnitRepository; + this.lectureUnitImportService = lectureUnitImportService; + this.courseCompetencyRepository = courseCompetencyRepository; + this.programmingExerciseTaskRepository = programmingExerciseTaskRepository; + this.gradingCriterionRepository = gradingCriterionRepository; + } + + /** + * Imports the related learning objects from the source course competencies into the course to import into and links them to the imported competencies. + * + * @param sourceCourseCompetencies The source course competencies to import from. + * @param idToImportedCompetency A map from the source competency IDs to the imported competencies. + * @param courseToImportInto The course to import the learning objects into. + * @param importOptions The import options. + */ + public void importRelatedLearningObjects(Collection sourceCourseCompetencies, Map idToImportedCompetency, + Course courseToImportInto, CompetencyImportOptionsDTO importOptions) { + Set importedCourseCompetencies = idToImportedCompetency.values().stream().map(CompetencyWithTailRelationDTO::competency).collect(Collectors.toSet()); + + Set importedExercises = new HashSet<>(); + if (importOptions.importExercises()) { + importOrLoadExercises(sourceCourseCompetencies, idToImportedCompetency, courseToImportInto, importedExercises); + } + Map titleToImportedLectures = new HashMap<>(); + Set importedLectureUnits = new HashSet<>(); + if (importOptions.importLectures()) { + importOrLoadLectureUnits(sourceCourseCompetencies, idToImportedCompetency, courseToImportInto, titleToImportedLectures, importedLectureUnits); + } + Set importedLectures = new HashSet<>(titleToImportedLectures.values()); + + if (importOptions.referenceDate().isPresent()) { + setAllDates(importedExercises, importedLectures, importedLectureUnits, importedCourseCompetencies, importOptions.referenceDate().get(), importOptions.isReleaseDate()); + } + + courseCompetencyRepository.saveAll(importedCourseCompetencies); + exerciseRepository.saveAll(importedExercises); + lectureRepository.saveAll(importedLectures); + } + + private void importOrLoadExercises(Collection sourceCourseCompetencies, Map idToImportedCompetency, + Course courseToImportInto, Set importedExercises) { + for (CourseCompetency sourceCourseCompetency : sourceCourseCompetencies) { + for (Exercise sourceExercise : sourceCourseCompetency.getExercises()) { + try { + Exercise importedExercise = importOrLoadExercise(sourceExercise, courseToImportInto); + + importedExercises.add(importedExercise); + + importedExercise.getCompetencies().add(idToImportedCompetency.get(sourceCourseCompetency.getId()).competency()); + idToImportedCompetency.get(sourceCourseCompetency.getId()).competency().getExercises().add(importedExercise); + } + catch (Exception e) { + log.error("Failed to import exercise with title {} together with its competency with id {}", sourceExercise.getTitle(), sourceCourseCompetency.getId(), e); + } + } + } + } + + private Exercise importOrLoadExercise(Exercise sourceExercise, Course course) throws JsonProcessingException { + return switch (sourceExercise) { + case ProgrammingExercise programmingExercise -> importOrLoadProgrammingExercise(programmingExercise, course); + case FileUploadExercise fileUploadExercise -> + importOrLoadExercise(fileUploadExercise, course, fileUploadExerciseRepository::findUniqueWithCompetenciesByTitleAndCourseId, + fileUploadExerciseRepository::findWithGradingCriteriaByIdElseThrow, fileUploadExerciseImportService::importFileUploadExercise); + case ModelingExercise modelingExercise -> importOrLoadExercise(modelingExercise, course, modelingExerciseRepository::findUniqueWithCompetenciesByTitleAndCourseId, + modelingExerciseRepository::findByIdWithExampleSubmissionsAndResultsAndPlagiarismDetectionConfigElseThrow, + modelingExerciseImportService::importModelingExercise); + case TextExercise textExercise -> importOrLoadExercise(textExercise, course, textExerciseRepository::findUniqueWithCompetenciesByTitleAndCourseId, + textExerciseRepository::findByIdWithExampleSubmissionsAndResultsAndGradingCriteriaElseThrow, textExerciseImportService::importTextExercise); + case QuizExercise quizExercise -> importOrLoadExercise(quizExercise, course, quizExerciseRepository::findUniqueWithCompetenciesByTitleAndCourseId, + quizExerciseRepository::findByIdWithQuestionsAndStatisticsAndCompetenciesAndBatchesAndGradingCriteriaElseThrow, (exercise, templateExercise) -> { + try { + return quizExerciseImportService.importQuizExercise(exercise, templateExercise, null); + } + catch (IOException e) { + throw new RuntimeException(e); + } + }); + default -> throw new IllegalStateException("Unexpected value: " + sourceExercise); + }; + } + + private Exercise importOrLoadProgrammingExercise(ProgrammingExercise programmingExercise, Course course) throws JsonProcessingException { + Optional foundByTitle = programmingExerciseRepository.findWithCompetenciesByTitleAndCourseId(programmingExercise.getTitle(), course.getId()); + Optional foundByShortName = programmingExerciseRepository.findByShortNameAndCourseIdWithCompetencies(programmingExercise.getShortName(), + course.getId()); + + if (foundByTitle.isPresent() && foundByShortName.isPresent() && !foundByTitle.get().equals(foundByShortName.get())) { + throw new IllegalArgumentException("Two programming exercises with the title or short name already exist in the course"); + } + + if (foundByTitle.isPresent()) { + return foundByTitle.get(); + } + else if (foundByShortName.isPresent()) { + return foundByShortName.get(); + } + else { + programmingExercise = programmingExerciseRepository.findByIdForImportElseThrow(programmingExercise.getId()); + // Fetching the tasks separately, as putting it in the query above leads to Hibernate duplicating the tasks. + var templateTasks = programmingExerciseTaskRepository.findByExerciseIdWithTestCases(programmingExercise.getId()); + programmingExercise.setTasks(new ArrayList<>(templateTasks)); + Set gradingCriteria = gradingCriterionRepository.findByExerciseIdWithEagerGradingCriteria(programmingExercise.getId()); + programmingExercise.setGradingCriteria(gradingCriteria); + + ProgrammingExercise newExercise = programmingExerciseRepository + .findByIdWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesAndCompetenciesAndPlagiarismDetectionConfigAndBuildConfigElseThrow( + programmingExercise.getId()); + PlagiarismDetectionConfigHelper.createAndSaveDefaultIfNullAndCourseExercise(newExercise, programmingExerciseRepository); + newExercise.setCourse(course); + newExercise.forceNewProjectKey(); + + clearProgrammingExerciseAttributes(newExercise); + + return programmingExerciseImportService.importProgrammingExercise(programmingExercise, newExercise, false, false, false); + } + } + + private void clearProgrammingExerciseAttributes(ProgrammingExercise programmingExercise) { + programmingExercise.setTasks(null); + programmingExercise.setExerciseHints(new HashSet<>()); + programmingExercise.setTestCases(new HashSet<>()); + programmingExercise.setStaticCodeAnalysisCategories(new HashSet<>()); + programmingExercise.setTeams(new HashSet<>()); + programmingExercise.setGradingCriteria(new HashSet<>()); + programmingExercise.setStudentParticipations(new HashSet<>()); + programmingExercise.setTutorParticipations(new HashSet<>()); + programmingExercise.setExampleSubmissions(new HashSet<>()); + programmingExercise.setAttachments(new HashSet<>()); + programmingExercise.setPosts(new HashSet<>()); + programmingExercise.setPlagiarismCases(new HashSet<>()); + programmingExercise.setCompetencies(new HashSet<>()); + } + + /** + * Imports or loads an exercise. + * + * @param exercise The source exercise for the import + * @param course The course to import the exercise into + * @param findFunction The function to find an existing exercise by title + * @param loadForImport The function to load an exercise for import + * @param importFunction The function to import the exercise + * @return The imported or loaded exercise + * @param The type of the exercise + */ + private Exercise importOrLoadExercise(E exercise, Course course, ThrowingBiFunction> findFunction, + Function loadForImport, BiFunction importFunction) { + Optional foundByTitle = findFunction.apply(exercise.getTitle(), course.getId()); + if (foundByTitle.isPresent()) { + return foundByTitle.get(); + } + else { + exercise = loadForImport.apply(exercise.getId()); + exercise.setCourse(course); + exercise.setId(null); + exercise.setCompetencies(new HashSet<>()); + + return importFunction.apply(exercise, exercise); + } + } + + /** + * Imports or loads a lecture unit. If the lecture unit needs to be imported, the lecture is imported or loaded as well. + * + * @param sourceCourseCompetencies The source course competencies to import from + * @param idToImportedCompetency A map from the source competency IDs to the imported competencies + * @param courseToImportInto The course to import the lecture unit into + * @param titleToImportedLectures A map from the source lecture titles to the imported lectures + * @param importedLectureUnits The set of imported lecture units + */ + private void importOrLoadLectureUnits(Collection sourceCourseCompetencies, Map idToImportedCompetency, + Course courseToImportInto, Map titleToImportedLectures, Set importedLectureUnits) { + for (CourseCompetency sourceCourseCompetency : sourceCourseCompetencies) { + for (LectureUnit sourceLectureUnit : sourceCourseCompetency.getLectureUnits()) { + try { + importOrLoadLectureUnit(sourceLectureUnit, sourceCourseCompetency, idToImportedCompetency, courseToImportInto, titleToImportedLectures, importedLectureUnits); + } + catch (Exception e) { + log.error("Failed to import lecture unit with name {} together with its competency with id {}", sourceLectureUnit.getName(), sourceCourseCompetency.getId(), e); + } + } + } + } + + private void importOrLoadLectureUnit(LectureUnit sourceLectureUnit, CourseCompetency sourceCourseCompetency, Map idToImportedCompetency, + Course courseToImportInto, Map titleToImportedLectures, Set importedLectureUnits) throws NoUniqueQueryException { + Lecture sourceLecture = sourceLectureUnit.getLecture(); + Lecture importedLecture = importOrLoadLecture(sourceLecture, courseToImportInto, titleToImportedLectures); + + Optional foundLectureUnit = lectureUnitRepository.findByNameAndLectureTitleAndCourseIdWithCompetencies(sourceLectureUnit.getName(), sourceLecture.getTitle(), + courseToImportInto.getId()); + LectureUnit importedLectureUnit; + if (foundLectureUnit.isEmpty()) { + importedLectureUnit = lectureUnitImportService.importLectureUnit(sourceLectureUnit); + + importedLecture.getLectureUnits().add(importedLectureUnit); + importedLectureUnit.setLecture(importedLecture); + } + else { + importedLectureUnit = foundLectureUnit.get(); + } + + importedLectureUnits.add(importedLectureUnit); + + importedLectureUnit.getCompetencies().add(idToImportedCompetency.get(sourceCourseCompetency.getId()).competency()); + idToImportedCompetency.get(sourceCourseCompetency.getId()).competency().getLectureUnits().add(importedLectureUnit); + } + + private Lecture importOrLoadLecture(Lecture sourceLecture, Course courseToImportInto, Map titleToImportedLectures) throws NoUniqueQueryException { + Optional foundLecture = Optional.ofNullable(titleToImportedLectures.get(sourceLecture.getTitle())); + if (foundLecture.isEmpty()) { + foundLecture = lectureRepository.findUniqueByTitleAndCourseIdWithLectureUnitsElseThrow(sourceLecture.getTitle(), courseToImportInto.getId()); + } + Lecture importedLecture = foundLecture.orElseGet(() -> lectureImportService.importLecture(sourceLecture, courseToImportInto, false)); + titleToImportedLectures.put(importedLecture.getTitle(), importedLecture); + + return importedLecture; + } + + private void setAllDates(Set importedExercises, Set importedLectures, Set importedLectureUnits, + Set importedCourseCompetencies, ZonedDateTime referenceDate, boolean isReleaseDate) { + long timeOffset = determineTimeOffset(importedExercises, importedLectures, importedLectureUnits, importedCourseCompetencies, referenceDate, isReleaseDate); + if (timeOffset == 0) { + return; + } + + importedExercises.forEach(exercise -> setAllExerciseDates(exercise, timeOffset)); + importedLectures.forEach(lecture -> setAllLectureDates(lecture, timeOffset)); + importedLectureUnits.forEach(lectureUnit -> setAllLectureUnitDates(lectureUnit, timeOffset)); + importedCourseCompetencies.forEach(competency -> setAllCompetencyDates(competency, timeOffset)); + } + + /** + * Finds the earliest relevant time and determines the time offset to apply to the dates of the imported learning objects. + * + * @param importedExercises The imported exercises + * @param importedLectures The imported lectures + * @param importedLectureUnits The imported lecture units + * @param importedCourseCompetencies The imported competencies + * @param referenceDate The reference date to calculate the offset from + * @param isReleaseDate Whether the offset is for the release date or the due date + * @return The time offset to apply + */ + private long determineTimeOffset(Set importedExercises, Set importedLectures, Set importedLectureUnits, + Set importedCourseCompetencies, ZonedDateTime referenceDate, boolean isReleaseDate) { + Optional earliestTime; + + if (isReleaseDate) { + Stream exerciseDates = importedExercises.stream().map(Exercise::getReleaseDate); + Stream lectureDates = importedLectures.stream().map(Lecture::getVisibleDate); + Stream lectureUnitDates = importedLectureUnits.stream().map(LectureUnit::getReleaseDate); + earliestTime = Stream.concat(exerciseDates, Stream.concat(lectureDates, lectureUnitDates)).filter(Objects::nonNull).min(Comparator.naturalOrder()); + } + else { + Stream exerciseDates = importedExercises.stream().map(Exercise::getDueDate); + Stream lectureDates = importedLectures.stream().map(Lecture::getEndDate); + Stream competencyDates = importedCourseCompetencies.stream().map(CourseCompetency::getSoftDueDate); + earliestTime = Stream.concat(exerciseDates, Stream.concat(lectureDates, competencyDates)).filter(Objects::nonNull).min(Comparator.naturalOrder()); + } + + return earliestTime.map(zonedDateTime -> referenceDate.toEpochSecond() - zonedDateTime.toEpochSecond()).orElse(0L); + } + + private void setAllExerciseDates(Exercise exercise, long timeOffset) { + if (exercise.getReleaseDate() != null) { + exercise.setReleaseDate(exercise.getReleaseDate().plusSeconds(timeOffset)); + } + if (exercise.getStartDate() != null) { + exercise.setStartDate(exercise.getStartDate().plusSeconds(timeOffset)); + } + if (exercise.getDueDate() != null) { + exercise.setDueDate(exercise.getDueDate().plusSeconds(timeOffset)); + } + if (exercise.getAssessmentDueDate() != null) { + exercise.setAssessmentDueDate(exercise.getAssessmentDueDate().plusSeconds(timeOffset)); + } + if (exercise.getExampleSolutionPublicationDate() != null) { + exercise.setExampleSolutionPublicationDate(exercise.getExampleSolutionPublicationDate().plusSeconds(timeOffset)); + } + + if (exercise instanceof QuizExercise quizExercise && !quizExercise.getQuizBatches().isEmpty()) { + quizExercise.getQuizBatches().forEach(batch -> { + if (batch.getStartTime() != null) { + batch.setStartTime(batch.getStartTime().plusSeconds(timeOffset)); + } + }); + } + + if (exercise instanceof ProgrammingExercise programmingExercise && programmingExercise.getBuildAndTestStudentSubmissionsAfterDueDate() != null) { + programmingExercise.setBuildAndTestStudentSubmissionsAfterDueDate(programmingExercise.getBuildAndTestStudentSubmissionsAfterDueDate().plusSeconds(timeOffset)); + } + } + + private void setAllLectureDates(Lecture lecture, long timeOffset) { + if (lecture.getVisibleDate() != null) { + lecture.setVisibleDate(lecture.getVisibleDate().plusSeconds(timeOffset)); + } + if (lecture.getStartDate() != null) { + lecture.setStartDate(lecture.getStartDate().plusSeconds(timeOffset)); + } + if (lecture.getEndDate() != null) { + lecture.setEndDate(lecture.getEndDate().plusSeconds(timeOffset)); + } + } + + private void setAllLectureUnitDates(LectureUnit lectureUnit, long timeOffset) { + if (lectureUnit.getReleaseDate() != null) { + lectureUnit.setReleaseDate(lectureUnit.getReleaseDate().plusSeconds(timeOffset)); + } + } + + private void setAllCompetencyDates(CourseCompetency competency, long timeOffset) { + if (competency.getSoftDueDate() != null) { + competency.setSoftDueDate(competency.getSoftDueDate().plusSeconds(timeOffset)); + } + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CompetencyService.java b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CompetencyService.java index 9217aa5196ad..9ec942846bf8 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CompetencyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CompetencyService.java @@ -13,12 +13,14 @@ import de.tum.cit.aet.artemis.atlas.domain.competency.Competency; import de.tum.cit.aet.artemis.atlas.domain.competency.CourseCompetency; +import de.tum.cit.aet.artemis.atlas.dto.CompetencyImportOptionsDTO; import de.tum.cit.aet.artemis.atlas.dto.CompetencyWithTailRelationDTO; import de.tum.cit.aet.artemis.atlas.repository.CompetencyProgressRepository; import de.tum.cit.aet.artemis.atlas.repository.CompetencyRelationRepository; import de.tum.cit.aet.artemis.atlas.repository.CompetencyRepository; import de.tum.cit.aet.artemis.atlas.repository.CourseCompetencyRepository; import de.tum.cit.aet.artemis.atlas.repository.StandardizedCompetencyRepository; +import de.tum.cit.aet.artemis.atlas.service.LearningObjectImportService; import de.tum.cit.aet.artemis.atlas.service.learningpath.LearningPathService; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; @@ -38,20 +40,22 @@ public class CompetencyService extends CourseCompetencyService { public CompetencyService(CompetencyRepository competencyRepository, AuthorizationCheckService authCheckService, CompetencyRelationRepository competencyRelationRepository, LearningPathService learningPathService, CompetencyProgressService competencyProgressService, LectureUnitService lectureUnitService, CompetencyProgressRepository competencyProgressRepository, LectureUnitCompletionRepository lectureUnitCompletionRepository, - StandardizedCompetencyRepository standardizedCompetencyRepository, CourseCompetencyRepository courseCompetencyRepository, ExerciseService exerciseService) { + StandardizedCompetencyRepository standardizedCompetencyRepository, CourseCompetencyRepository courseCompetencyRepository, ExerciseService exerciseService, + LearningObjectImportService learningObjectImportService) { super(competencyProgressRepository, courseCompetencyRepository, competencyRelationRepository, competencyProgressService, exerciseService, lectureUnitService, - learningPathService, authCheckService, standardizedCompetencyRepository, lectureUnitCompletionRepository); + learningPathService, authCheckService, standardizedCompetencyRepository, lectureUnitCompletionRepository, learningObjectImportService); this.competencyRepository = competencyRepository; } /** * Imports the given competencies and relations into a course * - * @param course the course to import into - * @param competencies the competencies to import + * @param course the course to import into + * @param competencies the competencies to import + * @param importOptions the options for importing the competencies * @return The set of imported competencies, each also containing the relations it is the tail competency for. */ - public Set importCompetenciesAndRelations(Course course, Collection competencies) { + public Set importCompetencies(Course course, Collection competencies, CompetencyImportOptionsDTO importOptions) { var idToImportedCompetency = new HashMap(); for (var competency : competencies) { @@ -62,7 +66,7 @@ public Set importCompetenciesAndRelations(Course idToImportedCompetency.put(competency.getId(), new CompetencyWithTailRelationDTO(importedCompetency, new ArrayList<>())); } - return importCourseCompetenciesAndRelations(course, idToImportedCompetency); + return importCourseCompetencies(course, competencies, idToImportedCompetency, importOptions); } /** @@ -76,17 +80,6 @@ public List importStandardizedCompetencies(List competen return super.importStandardizedCompetencies(competencyIdsToImport, course, Competency::new); } - /** - * Imports the given course competencies into a course - * - * @param course the course to import into - * @param competencies the course competencies to import - * @return The list of imported competencies - */ - public Set importCompetencies(Course course, Collection competencies) { - return importCourseCompetencies(course, competencies, Competency::new); - } - /** * Creates a new competency and links it to a course and lecture units. * diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CourseCompetencyService.java b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CourseCompetencyService.java index 05b4f0bca77e..01eb37cf8271 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CourseCompetencyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CourseCompetencyService.java @@ -24,12 +24,14 @@ import de.tum.cit.aet.artemis.atlas.domain.competency.CourseCompetency; import de.tum.cit.aet.artemis.atlas.domain.competency.Prerequisite; import de.tum.cit.aet.artemis.atlas.domain.competency.StandardizedCompetency; +import de.tum.cit.aet.artemis.atlas.dto.CompetencyImportOptionsDTO; import de.tum.cit.aet.artemis.atlas.dto.CompetencyRelationDTO; import de.tum.cit.aet.artemis.atlas.dto.CompetencyWithTailRelationDTO; import de.tum.cit.aet.artemis.atlas.repository.CompetencyProgressRepository; import de.tum.cit.aet.artemis.atlas.repository.CompetencyRelationRepository; import de.tum.cit.aet.artemis.atlas.repository.CourseCompetencyRepository; import de.tum.cit.aet.artemis.atlas.repository.StandardizedCompetencyRepository; +import de.tum.cit.aet.artemis.atlas.service.LearningObjectImportService; import de.tum.cit.aet.artemis.atlas.service.learningpath.LearningPathService; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.domain.User; @@ -73,10 +75,13 @@ public class CourseCompetencyService { protected final LectureUnitCompletionRepository lectureUnitCompletionRepository; + private final LearningObjectImportService learningObjectImportService; + public CourseCompetencyService(CompetencyProgressRepository competencyProgressRepository, CourseCompetencyRepository courseCompetencyRepository, CompetencyRelationRepository competencyRelationRepository, CompetencyProgressService competencyProgressService, ExerciseService exerciseService, LectureUnitService lectureUnitService, LearningPathService learningPathService, AuthorizationCheckService authCheckService, - StandardizedCompetencyRepository standardizedCompetencyRepository, LectureUnitCompletionRepository lectureUnitCompletionRepository) { + StandardizedCompetencyRepository standardizedCompetencyRepository, LectureUnitCompletionRepository lectureUnitCompletionRepository, + LearningObjectImportService learningObjectImportService) { this.competencyProgressRepository = competencyProgressRepository; this.courseCompetencyRepository = courseCompetencyRepository; this.competencyRelationRepository = competencyRelationRepository; @@ -87,6 +92,7 @@ public CourseCompetencyService(CompetencyProgressRepository competencyProgressRe this.authCheckService = authCheckService; this.standardizedCompetencyRepository = standardizedCompetencyRepository; this.lectureUnitCompletionRepository = lectureUnitCompletionRepository; + this.learningObjectImportService = learningObjectImportService; } /** @@ -158,9 +164,10 @@ public void filterOutLearningObjectsThatUserShouldNotSee(CourseCompetency compet * * @param course the course to import into * @param courseCompetencies the course competencies to import + * @param importOptions the import options * @return The set of imported course competencies, each also containing the relations it is the tail competency for. */ - public Set importCourseCompetenciesAndRelations(Course course, Collection courseCompetencies) { + public Set importCourseCompetencies(Course course, Collection courseCompetencies, CompetencyImportOptionsDTO importOptions) { var idToImportedCompetency = new HashMap(); for (var courseCompetency : courseCompetencies) { @@ -175,37 +182,45 @@ public Set importCourseCompetenciesAndRelations(C idToImportedCompetency.put(courseCompetency.getId(), new CompetencyWithTailRelationDTO(importedCompetency, new ArrayList<>())); } - return importCourseCompetenciesAndRelations(course, idToImportedCompetency); + return importCourseCompetencies(course, courseCompetencies, idToImportedCompetency, importOptions); } /** * Imports the given competencies and relations into a course * * @param course the course to import into + * @param competenciesToImport the source competencies that were imported * @param idToImportedCompetency map of original competency id to imported competency + * @param importOptions the import options * @return The set of imported competencies, each also containing the relations it is the tail competency for. */ - public Set importCourseCompetenciesAndRelations(Course course, Map idToImportedCompetency) { + public Set importCourseCompetencies(Course course, Collection competenciesToImport, + Map idToImportedCompetency, CompetencyImportOptionsDTO importOptions) { if (course.getLearningPathsEnabled()) { var importedCompetencies = idToImportedCompetency.values().stream().map(CompetencyWithTailRelationDTO::competency).toList(); learningPathService.linkCompetenciesToLearningPathsOfCourse(importedCompetencies, course.getId()); } - var originalCompetencyIds = idToImportedCompetency.keySet(); - var relations = competencyRelationRepository.findAllByHeadCompetencyIdInAndTailCompetencyIdIn(originalCompetencyIds, originalCompetencyIds); + if (importOptions.importRelations()) { + var originalCompetencyIds = idToImportedCompetency.keySet(); + var relations = competencyRelationRepository.findAllByHeadCompetencyIdInAndTailCompetencyIdIn(originalCompetencyIds, originalCompetencyIds); - for (var relation : relations) { - var tailCompetencyDTO = idToImportedCompetency.get(relation.getTailCompetency().getId()); - var headCompetencyDTO = idToImportedCompetency.get(relation.getHeadCompetency().getId()); + for (var relation : relations) { + var tailCompetencyDTO = idToImportedCompetency.get(relation.getTailCompetency().getId()); + var headCompetencyDTO = idToImportedCompetency.get(relation.getHeadCompetency().getId()); - CompetencyRelation relationToImport = new CompetencyRelation(); - relationToImport.setType(relation.getType()); - relationToImport.setTailCompetency(tailCompetencyDTO.competency()); - relationToImport.setHeadCompetency(headCompetencyDTO.competency()); + CompetencyRelation relationToImport = new CompetencyRelation(); + relationToImport.setType(relation.getType()); + relationToImport.setTailCompetency(tailCompetencyDTO.competency()); + relationToImport.setHeadCompetency(headCompetencyDTO.competency()); - relationToImport = competencyRelationRepository.save(relationToImport); - tailCompetencyDTO.tailRelations().add(CompetencyRelationDTO.of(relationToImport)); + relationToImport = competencyRelationRepository.save(relationToImport); + tailCompetencyDTO.tailRelations().add(CompetencyRelationDTO.of(relationToImport)); + } } + + learningObjectImportService.importRelatedLearningObjects(competenciesToImport, idToImportedCompetency, course, importOptions); + return new HashSet<>(idToImportedCompetency.values()); } @@ -247,51 +262,6 @@ public List importStandardizedCompetencies(List competen return importedCompetencies; } - /** - * Imports the given course competencies into a course - * - * @param course the course to import into - * @param courseCompetencies the course competencies to import - * @return The list of imported competencies - */ - public Set importCourseCompetencies(Course course, Collection courseCompetencies) { - Function courseCompetencyFunction = courseCompetency -> switch (courseCompetency) { - case Competency competency -> new Competency(competency); - case Prerequisite prerequisite -> new Prerequisite(prerequisite); - default -> throw new IllegalStateException("Unexpected value: " + courseCompetency); - }; - return importCourseCompetencies(course, courseCompetencies, courseCompetencyFunction); - } - - /** - * Imports the given course competencies into a course - * - * @param course the course to import into - * @param competencies the course competencies to import - * @param courseCompetencyFunction the function that creates new course competencies - * @return The set of imported competencies - */ - public Set importCourseCompetencies(Course course, Collection competencies, - Function courseCompetencyFunction) { - var importedCompetencies = new ArrayList(); - Set createdDTOs = new HashSet<>(); - - for (var competency : competencies) { - CourseCompetency importedCompetency = courseCompetencyFunction.apply(competency); - importedCompetency.setCourse(course); - - importedCompetency = courseCompetencyRepository.save(importedCompetency); - importedCompetencies.add(importedCompetency); - createdDTOs.add(new CompetencyWithTailRelationDTO(importedCompetency, Collections.emptyList())); - } - - if (course.getLearningPathsEnabled()) { - learningPathService.linkCompetenciesToLearningPathsOfCourse(importedCompetencies, course.getId()); - } - - return createdDTOs; - } - /** * Creates a new competency and links it to a course and lecture units. * If learning paths are enabled, the competency is also linked to the learning paths of the course. diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/LearningObjectImportService.java b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/LearningObjectImportService.java new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/PrerequisiteService.java b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/PrerequisiteService.java index aff6e8927bd7..3fc520a21378 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/PrerequisiteService.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/PrerequisiteService.java @@ -13,12 +13,14 @@ import de.tum.cit.aet.artemis.atlas.domain.competency.CourseCompetency; import de.tum.cit.aet.artemis.atlas.domain.competency.Prerequisite; +import de.tum.cit.aet.artemis.atlas.dto.CompetencyImportOptionsDTO; import de.tum.cit.aet.artemis.atlas.dto.CompetencyWithTailRelationDTO; import de.tum.cit.aet.artemis.atlas.repository.CompetencyProgressRepository; import de.tum.cit.aet.artemis.atlas.repository.CompetencyRelationRepository; import de.tum.cit.aet.artemis.atlas.repository.CourseCompetencyRepository; import de.tum.cit.aet.artemis.atlas.repository.PrerequisiteRepository; import de.tum.cit.aet.artemis.atlas.repository.StandardizedCompetencyRepository; +import de.tum.cit.aet.artemis.atlas.service.LearningObjectImportService; import de.tum.cit.aet.artemis.atlas.service.learningpath.LearningPathService; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; @@ -38,9 +40,10 @@ public class PrerequisiteService extends CourseCompetencyService { public PrerequisiteService(PrerequisiteRepository prerequisiteRepository, AuthorizationCheckService authCheckService, CompetencyRelationRepository competencyRelationRepository, LearningPathService learningPathService, CompetencyProgressService competencyProgressService, LectureUnitService lectureUnitService, CompetencyProgressRepository competencyProgressRepository, LectureUnitCompletionRepository lectureUnitCompletionRepository, - StandardizedCompetencyRepository standardizedCompetencyRepository, CourseCompetencyRepository courseCompetencyRepository, ExerciseService exerciseService) { + StandardizedCompetencyRepository standardizedCompetencyRepository, CourseCompetencyRepository courseCompetencyRepository, ExerciseService exerciseService, + LearningObjectImportService learningObjectImportService) { super(competencyProgressRepository, courseCompetencyRepository, competencyRelationRepository, competencyProgressService, exerciseService, lectureUnitService, - learningPathService, authCheckService, standardizedCompetencyRepository, lectureUnitCompletionRepository); + learningPathService, authCheckService, standardizedCompetencyRepository, lectureUnitCompletionRepository, learningObjectImportService); this.prerequisiteRepository = prerequisiteRepository; } @@ -49,9 +52,10 @@ public PrerequisiteService(PrerequisiteRepository prerequisiteRepository, Author * * @param course the course to import into * @param prerequisites the prerequisites to import + * @param importOptions the options for importing the prerequisites * @return The set of imported prerequisites, each also containing the relations for which it is the tail prerequisite for. */ - public Set importPrerequisitesAndRelations(Course course, Collection prerequisites) { + public Set importPrerequisites(Course course, Collection prerequisites, CompetencyImportOptionsDTO importOptions) { var idToImportedPrerequisite = new HashMap(); for (var prerequisite : prerequisites) { @@ -62,7 +66,7 @@ public Set importPrerequisitesAndRelations(Course idToImportedPrerequisite.put(prerequisite.getId(), new CompetencyWithTailRelationDTO(importedPrerequisite, new ArrayList<>())); } - return importCourseCompetenciesAndRelations(course, idToImportedPrerequisite); + return importCourseCompetencies(course, prerequisites, idToImportedPrerequisite, importOptions); } /** @@ -76,17 +80,6 @@ public List importStandardizedPrerequisites(List prerequ return super.importStandardizedCompetencies(prerequisiteIdsToImport, course, Prerequisite::new); } - /** - * Imports the given course prerequisites into a course - * - * @param course the course to import into - * @param prerequisites the course prerequisites to import - * @return The list of imported prerequisites - */ - public Set importPrerequisites(Course course, Collection prerequisites) { - return importCourseCompetencies(course, prerequisites, Prerequisite::new); - } - /** * Creates a new prerequisite and links it to a course and lecture units. * diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/service/learningpath/LearningPathService.java b/src/main/java/de/tum/cit/aet/artemis/atlas/service/learningpath/LearningPathService.java index 937123a30fe1..190565c5c35c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/service/learningpath/LearningPathService.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/service/learningpath/LearningPathService.java @@ -25,6 +25,7 @@ import de.tum.cit.aet.artemis.atlas.dto.CompetencyGraphEdgeDTO; import de.tum.cit.aet.artemis.atlas.dto.CompetencyGraphNodeDTO; import de.tum.cit.aet.artemis.atlas.dto.LearningPathCompetencyGraphDTO; +import de.tum.cit.aet.artemis.atlas.dto.LearningPathDTO; import de.tum.cit.aet.artemis.atlas.dto.LearningPathHealthDTO; import de.tum.cit.aet.artemis.atlas.dto.LearningPathInformationDTO; import de.tum.cit.aet.artemis.atlas.dto.LearningPathNavigationOverviewDTO; @@ -39,6 +40,7 @@ import de.tum.cit.aet.artemis.core.dto.SearchResultPageDTO; import de.tum.cit.aet.artemis.core.dto.pageablesearch.SearchTermPageableSearchDTO; import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException; +import de.tum.cit.aet.artemis.core.exception.ConflictException; 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.util.PageUtil; @@ -243,6 +245,52 @@ private void updateLearningPathProgress(@NotNull LearningPath learningPath) { log.debug("Updated LearningPath (id={}) for user (id={})", learningPath.getId(), userId); } + /** + * Get the learning path for the current user in the given course. + * + * @param courseId the id of the course + * @return the learning path of the current user + */ + public LearningPathDTO getLearningPathForCurrentUser(long courseId) { + final var currentUser = userRepository.getUser(); + final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(courseId, currentUser.getId()); + return LearningPathDTO.of(learningPath); + } + + /** + * Generate a learning path for the current user in the given course. + * + * @param courseId the id of the course + * @return the generated learning path + */ + public LearningPathDTO generateLearningPathForCurrentUser(long courseId) { + final var currentUser = userRepository.getUser(); + final var course = courseRepository.findWithEagerCompetenciesAndPrerequisitesByIdElseThrow(courseId); + if (learningPathRepository.findByCourseIdAndUserId(courseId, currentUser.getId()).isPresent()) { + throw new ConflictException("Learning path already exists.", "LearningPath", "learningPathAlreadyExists"); + } + final var learningPath = generateLearningPathForUser(course, currentUser); + return LearningPathDTO.of(learningPath); + } + + /** + * Start the learning path for the current user + * + * @param learningPathId the id of the learning path + */ + public void startLearningPathForCurrentUser(long learningPathId) { + final var learningPath = learningPathRepository.findByIdElseThrow(learningPathId); + final var currentUser = userRepository.getUser(); + if (!learningPath.getUser().equals(currentUser)) { + throw new AccessForbiddenException("You are not allowed to start this learning path."); + } + else if (learningPath.isStartedByStudent()) { + throw new ConflictException("Learning path already started.", "LearningPath", "learningPathAlreadyStarted"); + } + learningPath.setStartedByStudent(true); + learningPathRepository.save(learningPath); + } + /** * Gets the health status of learning paths for the given course. * diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/web/CompetencyResource.java b/src/main/java/de/tum/cit/aet/artemis/atlas/web/CompetencyResource.java index aa240c5f9a42..aa1a9f78dc0e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/web/CompetencyResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/web/CompetencyResource.java @@ -8,7 +8,6 @@ import java.util.Set; import jakarta.validation.constraints.NotNull; -import jakarta.ws.rs.BadRequestException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,11 +21,11 @@ import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import de.tum.cit.aet.artemis.atlas.domain.competency.Competency; import de.tum.cit.aet.artemis.atlas.domain.competency.CourseCompetency; +import de.tum.cit.aet.artemis.atlas.dto.CompetencyImportOptionsDTO; import de.tum.cit.aet.artemis.atlas.dto.CompetencyImportResponseDTO; import de.tum.cit.aet.artemis.atlas.dto.CompetencyWithTailRelationDTO; import de.tum.cit.aet.artemis.atlas.repository.CompetencyRepository; @@ -136,9 +135,8 @@ public ResponseEntity getCompetency(@PathVariable long competencyId, @EnforceAtLeastInstructorInCourse public ResponseEntity createCompetency(@PathVariable long courseId, @RequestBody Competency competency) throws URISyntaxException { log.debug("REST request to create Competency : {}", competency); - if (competency.getId() != null || competency.getTitle() == null || competency.getTitle().trim().isEmpty()) { - throw new BadRequestException(); - } + checkCompetencyAttributesForCreation(competency); + var course = courseRepository.findWithEagerCompetenciesAndPrerequisitesByIdElseThrow(courseId); final var persistedCompetency = competencyService.createCourseCompetency(competency, course); @@ -159,9 +157,7 @@ public ResponseEntity createCompetency(@PathVariable long courseId, public ResponseEntity> createCompetencies(@PathVariable Long courseId, @RequestBody List competencies) throws URISyntaxException { log.debug("REST request to create Competencies : {}", competencies); for (Competency competency : competencies) { - if (competency.getId() != null || competency.getTitle() == null || competency.getTitle().trim().isEmpty()) { - throw new BadRequestException(); - } + checkCompetencyAttributesForCreation(competency); } var course = courseRepository.findWithEagerCompetenciesAndPrerequisitesByIdElseThrow(courseId); @@ -173,25 +169,31 @@ public ResponseEntity> createCompetencies(@PathVariable Long co /** * POST courses/:courseId/competencies/import : imports a new competency. * - * @param courseId the id of the course to which the competency should be imported to - * @param competencyId the id of the competency that should be imported + * @param courseId the id of the course to which the competency should be imported to + * @param importOptions the options for the import * @return the ResponseEntity with status 201 (Created) and with body containing the imported competency * @throws URISyntaxException if the Location URI syntax is incorrect */ @PostMapping("courses/{courseId}/competencies/import") @EnforceAtLeastInstructorInCourse - public ResponseEntity importCompetency(@PathVariable long courseId, @RequestBody long competencyId) throws URISyntaxException { - log.info("REST request to import a competency: {}", competencyId); + public ResponseEntity importCompetency(@PathVariable long courseId, @RequestBody CompetencyImportOptionsDTO importOptions) throws URISyntaxException { + log.info("REST request to import a competency: {}", importOptions.competencyIds()); + + if (importOptions.competencyIds() == null || importOptions.competencyIds().size() != 1) { + throw new BadRequestAlertException("Exactly one competency must be imported", ENTITY_NAME, "noCompetency"); + } + long competencyId = importOptions.competencyIds().iterator().next(); var course = courseRepository.findWithEagerCompetenciesAndPrerequisitesByIdElseThrow(courseId); - var competencyToImport = courseCompetencyRepository.findByIdElseThrow(competencyId); + var competencyToImport = courseCompetencyRepository.findByIdWithExercisesAndLectureUnitsAndLecturesElseThrow(competencyId); authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, competencyToImport.getCourse(), null); if (competencyToImport.getCourse().getId().equals(courseId)) { throw new BadRequestAlertException("The competency is already added to this course", ENTITY_NAME, "competencyCycle"); } - Competency createdCompetency = competencyService.createCompetency(competencyToImport, course); + Set createdCompetencies = competencyService.importCompetencies(course, Set.of(competencyToImport), importOptions); + Competency createdCompetency = (Competency) createdCompetencies.iterator().next().competency(); return ResponseEntity.created(new URI("/api/courses/" + courseId + "/competencies/" + createdCompetency.getId())).body(createdCompetency); } @@ -199,21 +201,24 @@ public ResponseEntity importCompetency(@PathVariable long courseId, /** * POST courses/:courseId/competencies/import/bulk : imports a number of competencies (and optionally their relations) into a course. * - * @param courseId the id of the course to which the competencies should be imported to - * @param competencyIds the ids of the competencies that should be imported - * @param importRelations if relations should be imported as well + * @param courseId the id of the course to which the competencies should be imported to + * @param importOptions the options for the import * @return the ResponseEntity with status 201 (Created) and with body containing the imported competencies * @throws URISyntaxException if the Location URI syntax is incorrect */ @PostMapping("courses/{courseId}/competencies/import/bulk") @EnforceAtLeastEditorInCourse - public ResponseEntity> importCompetencies(@PathVariable long courseId, @RequestBody Set competencyIds, - @RequestParam(defaultValue = "false") boolean importRelations) throws URISyntaxException { - log.info("REST request to import competencies: {}", competencyIds); + public ResponseEntity> importCompetencies(@PathVariable long courseId, @RequestBody CompetencyImportOptionsDTO importOptions) + throws URISyntaxException { + log.info("REST request to import competencies: {}", importOptions.competencyIds()); + + if (importOptions.competencyIds() == null || importOptions.competencyIds().isEmpty()) { + throw new BadRequestAlertException("No competencies to import", ENTITY_NAME, "noCompetencies"); + } var course = courseRepository.findWithEagerCompetenciesAndPrerequisitesByIdElseThrow(courseId); - List competenciesToImport = courseCompetencyRepository.findAllById(competencyIds); + Set competenciesToImport = courseCompetencyRepository.findAllByIdWithExercisesAndLectureUnitsAndLecturesAndAttachments(importOptions.competencyIds()); User user = userRepository.getUserWithGroupsAndAuthorities(); competenciesToImport.forEach(competencyToImport -> { @@ -223,48 +228,37 @@ public ResponseEntity> importCompetencies(@Pa } }); - Set importedCompetencies; - if (importRelations) { - importedCompetencies = competencyService.importCompetenciesAndRelations(course, competenciesToImport); - } - else { - importedCompetencies = competencyService.importCompetencies(course, competenciesToImport); - } + Set importedCompetencies = competencyService.importCompetencies(course, competenciesToImport, importOptions); return ResponseEntity.created(new URI("/api/courses/" + courseId + "/competencies/")).body(importedCompetencies); } /** - * POST courses/{courseId}/competencies/import-all/{sourceCourseId} : Imports all competencies of the source course (and optionally their relations) into another. + * POST courses/{courseId}/competencies/import-all : Imports all competencies of the source course (and optionally their relations) into another. * - * @param courseId the id of the course to import into - * @param sourceCourseId the id of the course to import from - * @param importRelations if relations should be imported as well + * @param courseId the id of the course to import into + * @param importOptions the options for the import * @return the ResponseEntity with status 201 (Created) and with body containing the imported competencies (and relations) * @throws URISyntaxException if the Location URI syntax is incorrect */ - @PostMapping("courses/{courseId}/competencies/import-all/{sourceCourseId}") + @PostMapping("courses/{courseId}/competencies/import-all") @EnforceAtLeastInstructorInCourse - public ResponseEntity> importAllCompetenciesFromCourse(@PathVariable long courseId, @PathVariable long sourceCourseId, - @RequestParam(defaultValue = "false") boolean importRelations) throws URISyntaxException { - log.info("REST request to all competencies from course {} into course {}", sourceCourseId, courseId); + public ResponseEntity> importAllCompetenciesFromCourse(@PathVariable long courseId, @RequestBody CompetencyImportOptionsDTO importOptions) + throws URISyntaxException { + log.info("REST request to all competencies from course {} into course {}", importOptions.sourceCourseId(), courseId); - if (courseId == sourceCourseId) { - throw new BadRequestAlertException("Cannot import from a course into itself", "Course", "courseCycle"); + if (importOptions.sourceCourseId().isEmpty()) { + throw new BadRequestAlertException("No source course specified", ENTITY_NAME, "noSourceCourse"); + } + else if (courseId == importOptions.sourceCourseId().get()) { + throw new BadRequestAlertException("Cannot import from a course into itself", ENTITY_NAME, "courseCycle"); } var targetCourse = courseRepository.findByIdElseThrow(courseId); - var sourceCourse = courseRepository.findByIdElseThrow(sourceCourseId); + var sourceCourse = courseRepository.findByIdElseThrow(importOptions.sourceCourseId().get()); authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, sourceCourse, null); - var competencies = competencyRepository.findAllForCourse(sourceCourse.getId()); - Set importedCompetencies; - - if (importRelations) { - importedCompetencies = competencyService.importCompetenciesAndRelations(targetCourse, competencies); - } - else { - importedCompetencies = competencyService.importCompetencies(targetCourse, competencies); - } + var competencies = competencyRepository.findAllForCourseWithExercisesAndLectureUnitsAndLecturesAndAttachments(sourceCourse.getId()); + Set importedCompetencies = competencyService.importCompetencies(targetCourse, competencies, importOptions); return ResponseEntity.created(new URI("/api/courses/" + courseId + "/competencies/")).body(importedCompetencies); } @@ -300,9 +294,8 @@ public ResponseEntity> importStandardizedCompe @EnforceAtLeastInstructorInCourse public ResponseEntity updateCompetency(@PathVariable long courseId, @RequestBody Competency competency) { log.debug("REST request to update Competency : {}", competency); - if (competency.getId() == null) { - throw new BadRequestException(); - } + checkCompetencyAttributesForUpdate(competency); + var course = courseRepository.findByIdElseThrow(courseId); var existingCompetency = competencyRepository.findByIdWithLectureUnitsElseThrow(competency.getId()); checkCourseForCompetency(course, existingCompetency); @@ -334,6 +327,26 @@ public ResponseEntity deleteCompetency(@PathVariable long competencyId, @P return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, competency.getTitle())).build(); } + private void checkCompetencyAttributesForCreation(Competency competency) { + if (competency.getId() != null) { + throw new BadRequestAlertException("A new competency should not have an id", ENTITY_NAME, "existingCompetencyId"); + } + checkCompetencyAttributes(competency); + } + + private void checkCompetencyAttributesForUpdate(Competency competency) { + if (competency.getId() == null) { + throw new BadRequestAlertException("An updated competency should have an id", ENTITY_NAME, "missingCompetencyId"); + } + checkCompetencyAttributes(competency); + } + + private void checkCompetencyAttributes(Competency competency) { + if (competency.getTitle() == null || competency.getTitle().trim().isEmpty() || competency.getMasteryThreshold() < 1 || competency.getMasteryThreshold() > 100) { + throw new BadRequestAlertException("The attributes of the competency are invalid!", ENTITY_NAME, "invalidPrerequisiteAttributes"); + } + } + /** * Checks if the competency matches the course. * diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/web/CourseCompetencyResource.java b/src/main/java/de/tum/cit/aet/artemis/atlas/web/CourseCompetencyResource.java index d335c9285f59..449a92a1d171 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/web/CourseCompetencyResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/web/CourseCompetencyResource.java @@ -28,6 +28,7 @@ import de.tum.cit.aet.artemis.atlas.domain.competency.CompetencyProgress; import de.tum.cit.aet.artemis.atlas.domain.competency.CourseCompetency; +import de.tum.cit.aet.artemis.atlas.dto.CompetencyImportOptionsDTO; import de.tum.cit.aet.artemis.atlas.dto.CompetencyJolPairDTO; import de.tum.cit.aet.artemis.atlas.dto.CompetencyRelationDTO; import de.tum.cit.aet.artemis.atlas.dto.CompetencyWithTailRelationDTO; @@ -232,36 +233,31 @@ public ResponseEntity> getCompetenciesForI } /** - * POST courses/{courseId}/course-competencies/import-all/{sourceCourseId} : Imports all course competencies of the source course (and optionally their relations) into another. + * POST courses/{courseId}/course-competencies/import-all : Imports all course competencies of the source course (and optionally their relations) into another. * - * @param courseId the id of the course to import into - * @param sourceCourseId the id of the course to import from - * @param importRelations if relations should be imported as well + * @param courseId the id of the course to import into + * @param importOptions the options for the import * @return the ResponseEntity with status 201 (Created) and with body containing the imported competencies (and relations) * @throws URISyntaxException if the Location URI syntax is incorrect */ - @PostMapping("courses/{courseId}/course-competencies/import-all/{sourceCourseId}") + @PostMapping("courses/{courseId}/course-competencies/import-all") @EnforceAtLeastInstructorInCourse - public ResponseEntity> importAllCompetenciesFromCourse(@PathVariable long courseId, @PathVariable long sourceCourseId, - @RequestParam(defaultValue = "false") boolean importRelations) throws URISyntaxException { - log.info("REST request to all course competencies from course {} into course {}", sourceCourseId, courseId); + public ResponseEntity> importAllCompetenciesFromCourse(@PathVariable long courseId, @RequestBody CompetencyImportOptionsDTO importOptions) + throws URISyntaxException { + log.info("REST request to all course competencies from course {} into course {}", importOptions.sourceCourseId(), courseId); - if (courseId == sourceCourseId) { - throw new BadRequestAlertException("Cannot import from a course into itself", "Course", "courseCycle"); + if (importOptions.sourceCourseId().isEmpty()) { + throw new BadRequestAlertException("No source course specified", ENTITY_NAME, "noSourceCourse"); + } + else if (courseId == importOptions.sourceCourseId().get()) { + throw new BadRequestAlertException("Cannot import from a course into itself", ENTITY_NAME, "courseCycle"); } var targetCourse = courseRepository.findByIdElseThrow(courseId); - var sourceCourse = courseRepository.findByIdElseThrow(sourceCourseId); + var sourceCourse = courseRepository.findByIdElseThrow(importOptions.sourceCourseId().get()); authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, sourceCourse, null); - var competencies = courseCompetencyRepository.findAllForCourse(sourceCourse.getId()); - Set importedCompetencies; - - if (importRelations) { - importedCompetencies = courseCompetencyService.importCourseCompetenciesAndRelations(targetCourse, competencies); - } - else { - importedCompetencies = courseCompetencyService.importCourseCompetencies(targetCourse, competencies); - } + var competencies = courseCompetencyRepository.findAllForCourseWithExercisesAndLectureUnitsAndLecturesAndAttachments(sourceCourse.getId()); + Set importedCompetencies = courseCompetencyService.importCourseCompetencies(targetCourse, competencies, importOptions); return ResponseEntity.created(new URI("/api/courses/" + courseId + "/competencies/")).body(importedCompetencies); } diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/web/LearningPathResource.java b/src/main/java/de/tum/cit/aet/artemis/atlas/web/LearningPathResource.java index 709ac913cdf9..f69dae28f80c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/web/LearningPathResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/web/LearningPathResource.java @@ -17,6 +17,7 @@ import org.springframework.context.annotation.Profile; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; @@ -28,6 +29,7 @@ import de.tum.cit.aet.artemis.atlas.dto.CompetencyNameDTO; import de.tum.cit.aet.artemis.atlas.dto.CompetencyProgressForLearningPathDTO; import de.tum.cit.aet.artemis.atlas.dto.LearningPathCompetencyGraphDTO; +import de.tum.cit.aet.artemis.atlas.dto.LearningPathDTO; import de.tum.cit.aet.artemis.atlas.dto.LearningPathHealthDTO; import de.tum.cit.aet.artemis.atlas.dto.LearningPathInformationDTO; import de.tum.cit.aet.artemis.atlas.dto.LearningPathNavigationDTO; @@ -57,6 +59,7 @@ import de.tum.cit.aet.artemis.lecture.service.LearningObjectService; @Profile(PROFILE_CORE) +@FeatureToggle(Feature.LearningPaths) @RestController @RequestMapping("api/") public class LearningPathResource { @@ -106,7 +109,6 @@ public LearningPathResource(CourseService courseService, CourseRepository course * @return the ResponseEntity with status 200 (OK) */ @PutMapping("courses/{courseId}/learning-paths/enable") - @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastInstructorInCourse public ResponseEntity enableLearningPathsForCourse(@PathVariable long courseId) { log.debug("REST request to enable learning paths for course with id: {}", courseId); @@ -127,7 +129,6 @@ public ResponseEntity enableLearningPathsForCourse(@PathVariable long cour * @return the ResponseEntity with status 200 (OK) */ @PutMapping("courses/{courseId}/learning-paths/generate-missing") - @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastInstructorInCourse public ResponseEntity generateMissingLearningPathsForCourse(@PathVariable long courseId) { log.debug("REST request to generate missing learning paths for course with id: {}", courseId); @@ -145,7 +146,6 @@ public ResponseEntity generateMissingLearningPathsForCourse(@PathVariable * @return the ResponseEntity with status 200 (OK) and with body the desired page, sorted and matching the given query */ @GetMapping("courses/{courseId}/learning-paths") - @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastInstructorInCourse public ResponseEntity> getLearningPathsOnPage(@PathVariable long courseId, SearchTermPageableSearchDTO search) { log.debug("REST request to get learning paths for course with id: {}", courseId); @@ -160,7 +160,6 @@ public ResponseEntity> getLearni * @return the ResponseEntity with status 200 (OK) and with body the health status */ @GetMapping("courses/{courseId}/learning-path-health") - @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastInstructorInCourse public ResponseEntity getHealthStatusForCourse(@PathVariable long courseId) { log.debug("REST request to get health status of learning paths in course with id: {}", courseId); @@ -175,7 +174,6 @@ public ResponseEntity getHealthStatusForCourse(@PathVaria * @return the ResponseEntity with status 200 (OK) and with body the learning path */ @GetMapping("learning-path/{learningPathId}") - @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastStudent public ResponseEntity getLearningPath(@PathVariable long learningPathId) { log.debug("REST request to get learning path with id: {}", learningPathId); @@ -194,7 +192,6 @@ public ResponseEntity getLearningPath(@PathVariable * @return the ResponseEntity with status 200 (OK) and with body the graph */ @GetMapping("learning-path/{learningPathId}/competency-graph") - @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastStudent public ResponseEntity getLearningPathCompetencyGraph(@PathVariable long learningPathId) { log.debug("REST request to get competency graph for learning path with id: {}", learningPathId); @@ -213,7 +210,6 @@ public ResponseEntity getLearningPathCompetencyG * @return the ResponseEntity with status 200 (OK) and with body the ngx representation of the learning path */ @GetMapping("learning-path/{learningPathId}/graph") - @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastStudent public ResponseEntity getLearningPathNgxGraph(@PathVariable long learningPathId) { log.debug("REST request to get ngx graph representation of learning path with id: {}", learningPathId); @@ -227,7 +223,6 @@ public ResponseEntity getLearningPathNgxGraph(@PathVariable * @return the ResponseEntity with status 200 (OK) and with body the ngx representation of the learning path */ @GetMapping("learning-path/{learningPathId}/path") - @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastStudent public ResponseEntity getLearningPathNgxPath(@PathVariable long learningPathId) { log.debug("REST request to get ngx path representation of learning path with id: {}", learningPathId); @@ -244,7 +239,6 @@ public ResponseEntity getLearningPathNgxPath(@PathVariable l * @return the ResponseEntity with status 200 (OK) and with body the navigation information */ @GetMapping("learning-path/{learningPathId}/relative-navigation") - @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastStudent public ResponseEntity getRelativeLearningPathNavigation(@PathVariable @Valid long learningPathId, @RequestParam long learningObjectId, @RequestParam LearningObjectType learningObjectType, @RequestParam long competencyId) { @@ -263,7 +257,6 @@ public ResponseEntity getRelativeLearningPathNavigati * @return the ResponseEntity with status 200 (OK) and with body the navigation information */ @GetMapping("learning-path/{learningPathId}/navigation") - @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastStudent public ResponseEntity getLearningPathNavigation(@PathVariable long learningPathId) { log.debug("REST request to get navigation for learning path with id: {}", learningPathId); @@ -279,7 +272,6 @@ public ResponseEntity getLearningPathNavigation(@Path * @return the ResponseEntity with status 200 (OK) and with body the navigation overview */ @GetMapping("learning-path/{learningPathId}/navigation-overview") - @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastStudent public ResponseEntity getLearningPathNavigationOverview(@PathVariable @Valid long learningPathId) { log.debug("REST request to get navigation overview for learning path with id: {}", learningPathId); @@ -301,19 +293,31 @@ private ResponseEntity getLearningPathNgx(@PathVariable long } /** - * GET courses/:courseId/learning-path-id : Gets the id of the learning path. + * GET courses/:courseId/learning-path/me : Gets the learning path of the current user in the course. * - * @param courseId the id of the course from which the learning path id should be fetched - * @return the ResponseEntity with status 200 (OK) and with body the id of the learning path + * @param courseId the id of the course for which the learning path should be fetched + * @return the ResponseEntity with status 200 (OK) and with body the learning path */ - @GetMapping("courses/{courseId}/learning-path-id") + @GetMapping("courses/{courseId}/learning-path/me") @EnforceAtLeastStudentInCourse - public ResponseEntity getLearningPathId(@PathVariable long courseId) { - log.debug("REST request to get learning path id for course with id: {}", courseId); + public ResponseEntity getLearningPathForCurrentUser(@PathVariable long courseId) { + log.debug("REST request to get learning path of current user for course with id: {}", courseId); courseService.checkLearningPathsEnabledElseThrow(courseId); - User user = userRepository.getUser(); - final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(courseId, user.getId()); - return ResponseEntity.ok(learningPath.getId()); + return ResponseEntity.ok(learningPathService.getLearningPathForCurrentUser(courseId)); + } + + /** + * PATCH learning-path/:learningPathId/start : Starts the learning path for the current user. + * + * @param learningPathId the id of the learning path to start + * @return the ResponseEntity with status 204 (NO_CONTENT) + */ + @PatchMapping("learning-path/{learningPathId}/start") + @EnforceAtLeastStudent + public ResponseEntity startLearningPathForCurrentUser(@PathVariable long learningPathId) { + log.debug("REST request to start learning path with id: {}", learningPathId); + learningPathService.startLearningPathForCurrentUser(learningPathId); + return ResponseEntity.noContent().build(); } /** @@ -324,20 +328,11 @@ public ResponseEntity getLearningPathId(@PathVariable long courseId) { */ @PostMapping("courses/{courseId}/learning-path") @EnforceAtLeastStudentInCourse - public ResponseEntity generateLearningPath(@PathVariable long courseId) throws URISyntaxException { - log.debug("REST request to generate learning path for user in course with id: {}", courseId); + public ResponseEntity generateLearningPathForCurrentUser(@PathVariable long courseId) throws URISyntaxException { + log.debug("REST request to generate learning path for current user in course with id: {}", courseId); courseService.checkLearningPathsEnabledElseThrow(courseId); - - User user = userRepository.getUser(); - final var learningPathOptional = learningPathRepository.findByCourseIdAndUserId(courseId, user.getId()); - - if (learningPathOptional.isPresent()) { - throw new BadRequestException("Learning path already exists."); - } - - final var course = courseRepository.findWithEagerCompetenciesAndPrerequisitesByIdElseThrow(courseId); - final var learningPath = learningPathService.generateLearningPathForUser(course, user); - return ResponseEntity.created(new URI("api/learning-path/" + learningPath.getId())).body(learningPath.getId()); + final var learningPathDTO = learningPathService.generateLearningPathForCurrentUser(courseId); + return ResponseEntity.created(new URI("api/learning-path/" + learningPathDTO.id())).body(learningPathDTO); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/web/PrerequisiteResource.java b/src/main/java/de/tum/cit/aet/artemis/atlas/web/PrerequisiteResource.java index 7cd32bdd957a..a9be53c3c4a4 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/web/PrerequisiteResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/web/PrerequisiteResource.java @@ -8,7 +8,6 @@ import java.util.Set; import jakarta.validation.constraints.NotNull; -import jakarta.ws.rs.BadRequestException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,11 +21,11 @@ import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import de.tum.cit.aet.artemis.atlas.domain.competency.CourseCompetency; import de.tum.cit.aet.artemis.atlas.domain.competency.Prerequisite; +import de.tum.cit.aet.artemis.atlas.dto.CompetencyImportOptionsDTO; import de.tum.cit.aet.artemis.atlas.dto.CompetencyImportResponseDTO; import de.tum.cit.aet.artemis.atlas.dto.CompetencyWithTailRelationDTO; import de.tum.cit.aet.artemis.atlas.repository.CourseCompetencyRepository; @@ -39,6 +38,7 @@ 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.Role; +import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInCourse.EnforceAtLeastEditorInCourse; import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInCourse.EnforceAtLeastInstructorInCourse; import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInCourse.EnforceAtLeastStudentInCourse; @@ -97,7 +97,7 @@ public PrerequisiteResource(CourseRepository courseRepository, AuthorizationChec * @return the ResponseEntity with status 200 (OK) and with body the found prerequisites */ @GetMapping("courses/{courseId}/prerequisites") - @EnforceAtLeastStudentInCourse + @EnforceAtLeastStudent public ResponseEntity> getPrerequisitesWithProgress(@PathVariable long courseId) { log.debug("REST request to get prerequisites for course with id: {}", courseId); User user = userRepository.getUserWithGroupsAndAuthorities(); @@ -139,9 +139,8 @@ public ResponseEntity getPrerequisite(@PathVariable long prerequis @EnforceAtLeastInstructorInCourse public ResponseEntity createPrerequisite(@PathVariable long courseId, @RequestBody Prerequisite prerequisite) throws URISyntaxException { log.debug("REST request to create Prerequisite : {}", prerequisite); - if (prerequisite.getId() != null || prerequisite.getTitle() == null || prerequisite.getTitle().trim().isEmpty()) { - throw new BadRequestException(); - } + checkPrerequisitesAttributesForCreation(prerequisite); + var course = courseRepository.findWithEagerCompetenciesAndPrerequisitesByIdElseThrow(courseId); final var persistedPrerequisite = prerequisiteService.createCourseCompetency(prerequisite, course); @@ -162,9 +161,7 @@ public ResponseEntity createPrerequisite(@PathVariable long course public ResponseEntity> createPrerequisite(@PathVariable Long courseId, @RequestBody List prerequisites) throws URISyntaxException { log.debug("REST request to create Prerequisites : {}", prerequisites); for (Prerequisite prerequisite : prerequisites) { - if (prerequisite.getId() != null || prerequisite.getTitle() == null || prerequisite.getTitle().trim().isEmpty()) { - throw new BadRequestException(); - } + checkPrerequisitesAttributesForCreation(prerequisite); } var course = courseRepository.findWithEagerCompetenciesAndPrerequisitesByIdElseThrow(courseId); @@ -176,25 +173,31 @@ public ResponseEntity> createPrerequisite(@PathVariable Long /** * POST courses/:courseId/prerequisites/import : imports a new prerequisite. * - * @param courseId the id of the course to which the prerequisite should be imported to - * @param prerequisiteId the id of the prerequisite that should be imported + * @param courseId the id of the course to which the prerequisite should be imported to + * @param importOptions the options for the import * @return the ResponseEntity with status 201 (Created) and with body containing the imported prerequisite * @throws URISyntaxException if the Location URI syntax is incorrect */ @PostMapping("courses/{courseId}/prerequisites/import") @EnforceAtLeastInstructorInCourse - public ResponseEntity importPrerequisite(@PathVariable long courseId, @RequestBody long prerequisiteId) throws URISyntaxException { - log.info("REST request to import a prerequisite: {}", prerequisiteId); + public ResponseEntity importPrerequisite(@PathVariable long courseId, @RequestBody CompetencyImportOptionsDTO importOptions) throws URISyntaxException { + log.info("REST request to import a prerequisite: {}", importOptions.competencyIds()); + + if (importOptions.competencyIds() == null || importOptions.competencyIds().size() != 1) { + throw new BadRequestAlertException("Exactly one prerequisite must be imported", ENTITY_NAME, "noPrerequisite"); + } + long prerequisiteId = importOptions.competencyIds().iterator().next(); var course = courseRepository.findWithEagerCompetenciesAndPrerequisitesByIdElseThrow(courseId); - var prerequisiteToImport = courseCompetencyRepository.findByIdElseThrow(prerequisiteId); + var prerequisiteToImport = courseCompetencyRepository.findByIdWithExercisesAndLectureUnitsAndLecturesElseThrow(prerequisiteId); authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, prerequisiteToImport.getCourse(), null); if (prerequisiteToImport.getCourse().getId().equals(courseId)) { throw new BadRequestAlertException("The prerequisite is already added to this course", ENTITY_NAME, "prerequisiteCycle"); } - Prerequisite createdPrerequisite = prerequisiteService.createPrerequisite(prerequisiteToImport, course); + Set createdPrerequisites = prerequisiteService.importPrerequisites(course, Set.of(prerequisiteToImport), importOptions); + Prerequisite createdPrerequisite = (Prerequisite) createdPrerequisites.iterator().next().competency(); return ResponseEntity.created(new URI("/api/courses/" + courseId + "/prerequisites/" + createdPrerequisite.getId())).body(createdPrerequisite); } @@ -202,21 +205,24 @@ public ResponseEntity importPrerequisite(@PathVariable long course /** * POST courses/:courseId/prerequisites/import/bulk : imports a number of prerequisites (and optionally their relations) into a course. * - * @param courseId the id of the course to which the prerequisites should be imported to - * @param prerequisiteIds the ids of the prerequisites that should be imported - * @param importRelations if relations should be imported as well + * @param courseId the id of the course to which the prerequisites should be imported to + * @param importOptions the options for the import * @return the ResponseEntity with status 201 (Created) and with body containing the imported prerequisites * @throws URISyntaxException if the Location URI syntax is incorrect */ @PostMapping("courses/{courseId}/prerequisites/import/bulk") @EnforceAtLeastEditorInCourse - public ResponseEntity> importPrerequisites(@PathVariable long courseId, @RequestBody Set prerequisiteIds, - @RequestParam(defaultValue = "false") boolean importRelations) throws URISyntaxException { - log.info("REST request to import prerequisites: {}", prerequisiteIds); + public ResponseEntity> importPrerequisites(@PathVariable long courseId, @RequestBody CompetencyImportOptionsDTO importOptions) + throws URISyntaxException { + log.info("REST request to import prerequisites: {}", importOptions.competencyIds()); + + if (importOptions.competencyIds() == null || importOptions.competencyIds().isEmpty()) { + throw new BadRequestAlertException("No prerequisites to import", ENTITY_NAME, "noPrerequisites"); + } var course = courseRepository.findWithEagerCompetenciesAndPrerequisitesByIdElseThrow(courseId); - List prerequisitesToImport = courseCompetencyRepository.findAllById(prerequisiteIds); + Set prerequisitesToImport = courseCompetencyRepository.findAllByIdWithExercisesAndLectureUnitsAndLecturesAndAttachments(importOptions.competencyIds()); User user = userRepository.getUserWithGroupsAndAuthorities(); prerequisitesToImport.forEach(prerequisiteToImport -> { @@ -226,48 +232,37 @@ public ResponseEntity> importPrerequisites(@P } }); - Set importedPrerequisites; - if (importRelations) { - importedPrerequisites = prerequisiteService.importPrerequisitesAndRelations(course, prerequisitesToImport); - } - else { - importedPrerequisites = prerequisiteService.importPrerequisites(course, prerequisitesToImport); - } + Set importedPrerequisites = prerequisiteService.importPrerequisites(course, prerequisitesToImport, importOptions); return ResponseEntity.created(new URI("/api/courses/" + courseId + "/prerequisites/")).body(importedPrerequisites); } /** - * POST courses/{courseId}/prerequisites/import-all/{sourceCourseId} : Imports all prerequisites of the source course (and optionally their relations) into another. + * POST courses/{courseId}/prerequisites/import-all : Imports all prerequisites of the source course (and optionally their relations) into another. * - * @param courseId the id of the course to import into - * @param sourceCourseId the id of the course to import from - * @param importRelations if relations should be imported as well + * @param courseId the id of the course to import into + * @param importOptions the options for the import * @return the ResponseEntity with status 201 (Created) and with body containing the imported prerequisites (and relations) * @throws URISyntaxException if the Location URI syntax is incorrect */ - @PostMapping("courses/{courseId}/prerequisites/import-all/{sourceCourseId}") + @PostMapping("courses/{courseId}/prerequisites/import-all") @EnforceAtLeastInstructorInCourse - public ResponseEntity> importAllPrerequisitesFromCourse(@PathVariable long courseId, @PathVariable long sourceCourseId, - @RequestParam(defaultValue = "false") boolean importRelations) throws URISyntaxException { - log.info("REST request to all prerequisites from course {} into course {}", sourceCourseId, courseId); + public ResponseEntity> importAllPrerequisitesFromCourse(@PathVariable long courseId, @RequestBody CompetencyImportOptionsDTO importOptions) + throws URISyntaxException { + log.info("REST request to all prerequisites from course {} into course {}", importOptions.sourceCourseId(), courseId); - if (courseId == sourceCourseId) { - throw new BadRequestAlertException("Cannot import from a course into itself", "Course", "courseCycle"); + if (importOptions.sourceCourseId().isEmpty()) { + throw new BadRequestAlertException("No source course specified", ENTITY_NAME, "noSourceCourse"); + } + else if (courseId == importOptions.sourceCourseId().get()) { + throw new BadRequestAlertException("Cannot import from a course into itself", ENTITY_NAME, "courseCycle"); } var targetCourse = courseRepository.findByIdElseThrow(courseId); - var sourceCourse = courseRepository.findByIdElseThrow(sourceCourseId); + var sourceCourse = courseRepository.findByIdElseThrow(importOptions.sourceCourseId().get()); authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, sourceCourse, null); - var prerequisites = prerequisiteRepository.findAllForCourse(sourceCourse.getId()); - Set importedPrerequisites; - - if (importRelations) { - importedPrerequisites = prerequisiteService.importPrerequisitesAndRelations(targetCourse, prerequisites); - } - else { - importedPrerequisites = prerequisiteService.importPrerequisites(targetCourse, prerequisites); - } + var prerequisites = prerequisiteRepository.findAllForCourseWithExercisesAndLectureUnitsAndLecturesAndAttachments(sourceCourse.getId()); + Set importedPrerequisites = prerequisiteService.importPrerequisites(targetCourse, prerequisites, importOptions); return ResponseEntity.created(new URI("/api/courses/" + courseId + "/prerequisites/")).body(importedPrerequisites); } @@ -303,9 +298,8 @@ public ResponseEntity> importStandardizedPrere @EnforceAtLeastInstructorInCourse public ResponseEntity updatePrerequisite(@PathVariable long courseId, @RequestBody Prerequisite prerequisite) { log.debug("REST request to update Prerequisite : {}", prerequisite); - if (prerequisite.getId() == null) { - throw new BadRequestException(); - } + checkPrerequisitesAttributesForUpdate(prerequisite); + var course = courseRepository.findByIdElseThrow(courseId); var existingPrerequisite = prerequisiteRepository.findByIdWithLectureUnitsElseThrow(prerequisite.getId()); checkCourseForPrerequisite(course, existingPrerequisite); @@ -337,6 +331,26 @@ public ResponseEntity deletePrerequisite(@PathVariable long prerequisiteId return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, prerequisite.getTitle())).build(); } + private void checkPrerequisitesAttributesForCreation(Prerequisite prerequisite) { + if (prerequisite.getId() != null) { + throw new BadRequestAlertException("A new prerequiste should not have an id", ENTITY_NAME, "existingPrerequisiteId"); + } + checkPrerequisitesAttributes(prerequisite); + } + + private void checkPrerequisitesAttributesForUpdate(Prerequisite prerequisite) { + if (prerequisite.getId() == null) { + throw new BadRequestAlertException("An updated prerequiste should have an id", ENTITY_NAME, "missingPrerequisiteId"); + } + checkPrerequisitesAttributes(prerequisite); + } + + private void checkPrerequisitesAttributes(Prerequisite prerequisite) { + if (prerequisite.getTitle() == null || prerequisite.getTitle().trim().isEmpty() || prerequisite.getMasteryThreshold() < 1 || prerequisite.getMasteryThreshold() > 100) { + throw new BadRequestAlertException("The attributes of the competency are invalid!", ENTITY_NAME, "invalidPrerequisiteAttributes"); + } + } + /** * Checks if the prerequisite matches the course. * diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/web/ScienceResource.java b/src/main/java/de/tum/cit/aet/artemis/atlas/web/ScienceResource.java index 7312cc824997..d4e1563df41c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/web/ScienceResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/web/ScienceResource.java @@ -21,6 +21,7 @@ * REST controller providing the science related endpoints. */ @Profile(PROFILE_CORE) +@FeatureToggle(Feature.Science) @RestController @RequestMapping("api/") public class ScienceResource { @@ -40,7 +41,6 @@ public ScienceResource(ScienceEventService scienceEventService) { * @return the ResponseEntity with status 200 (OK) */ @PutMapping(value = "science") - @FeatureToggle(Feature.Science) @EnforceAtLeastStudent public ResponseEntity science(@RequestBody ScienceEventDTO event) { log.debug("REST request to log science event of type {}", event); diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/web/StandardizedCompetencyResource.java b/src/main/java/de/tum/cit/aet/artemis/atlas/web/StandardizedCompetencyResource.java index 8b6316fcc51c..b205db140bc3 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/web/StandardizedCompetencyResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/web/StandardizedCompetencyResource.java @@ -29,6 +29,7 @@ * REST controller for managing {@link StandardizedCompetency} entities. */ @Profile(PROFILE_CORE) +@FeatureToggle(Feature.StandardizedCompetencies) @RestController @RequestMapping("api/standardized-competencies/") public class StandardizedCompetencyResource { @@ -58,7 +59,6 @@ public StandardizedCompetencyResource(StandardizedCompetencyService standardized * @return the ResponseEntity with status 200 (OK) and with body containing the standardized competency, or with status 404 (Not Found) */ @GetMapping("{competencyId}") - @FeatureToggle(Feature.StandardizedCompetencies) @EnforceAtLeastInstructor public ResponseEntity getStandardizedCompetency(@PathVariable long competencyId) { log.debug("REST request to get standardized competency with id : {}", competencyId); @@ -74,7 +74,6 @@ public ResponseEntity getStandardizedCompetency(@PathVar * @return the ResponseEntity with status 200 (OK) and with body containing the knowledge areas */ @GetMapping("for-tree-view") - @FeatureToggle(Feature.StandardizedCompetencies) @EnforceAtLeastInstructor public ResponseEntity> getAllForTreeView() { log.debug("REST request to all knowledge areas for tree view"); @@ -91,7 +90,6 @@ public ResponseEntity> getAllForTreeView() { * @return the ResponseEntity with status 200 (OK) and with body containing the knowledge area, or with status 404 (Not Found) */ @GetMapping("knowledge-areas/{knowledgeAreaId}") - @FeatureToggle(Feature.StandardizedCompetencies) @EnforceAtLeastInstructor public ResponseEntity getKnowledgeArea(@PathVariable long knowledgeAreaId) { log.debug("REST request to get knowledge area with id : {}", knowledgeAreaId); @@ -107,7 +105,6 @@ public ResponseEntity getKnowledgeArea(@PathVariable long knowled * @return the ResponseEntity with status 200 (OK) and with body containing the list of sources */ @GetMapping("sources") - @FeatureToggle(Feature.StandardizedCompetencies) @EnforceAtLeastInstructor public ResponseEntity> getSources() { log.debug("REST request to get all sources"); diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/web/admin/AdminStandardizedCompetencyResource.java b/src/main/java/de/tum/cit/aet/artemis/atlas/web/admin/AdminStandardizedCompetencyResource.java index 376cedb132dc..5c974101f8bb 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/web/admin/AdminStandardizedCompetencyResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/web/admin/AdminStandardizedCompetencyResource.java @@ -38,6 +38,8 @@ * Admin REST controller for managing {@link StandardizedCompetency} entities. */ @Profile(PROFILE_CORE) +@FeatureToggle(Feature.StandardizedCompetencies) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminStandardizedCompetencyResource { @@ -61,8 +63,6 @@ public AdminStandardizedCompetencyResource(StandardizedCompetencyService standar * @throws URISyntaxException if the Location URI syntax is incorrect */ @PostMapping("standardized-competencies") - @FeatureToggle(Feature.StandardizedCompetencies) - @EnforceAdmin public ResponseEntity createStandardizedCompetency(@RequestBody @Valid StandardizedCompetencyRequestDTO competency) throws URISyntaxException { log.debug("REST request to create standardized competency : {}", competency); @@ -79,8 +79,6 @@ public ResponseEntity createStandardizedCompete * @return the ResponseEntity with status 200 (OK) and with body the updated standardized competency */ @PutMapping("standardized-competencies/{competencyId}") - @FeatureToggle(Feature.StandardizedCompetencies) - @EnforceAdmin public ResponseEntity updateStandardizedCompetency(@PathVariable long competencyId, @RequestBody @Valid StandardizedCompetencyRequestDTO competency) { log.debug("REST request to update standardized competency : {}", competency); @@ -97,8 +95,6 @@ public ResponseEntity updateStandardizedCompete * @return the ResponseEntity with status 200 (OK) */ @DeleteMapping("standardized-competencies/{competencyId}") - @FeatureToggle(Feature.StandardizedCompetencies) - @EnforceAdmin public ResponseEntity deleteStandardizedCompetency(@PathVariable long competencyId) { log.debug("REST request to delete standardized competency : {}", competencyId); @@ -115,8 +111,6 @@ public ResponseEntity deleteStandardizedCompetency(@PathVariable long comp * @throws URISyntaxException if the Location URI syntax is incorrect */ @PostMapping("standardized-competencies/knowledge-areas") - @FeatureToggle(Feature.StandardizedCompetencies) - @EnforceAdmin public ResponseEntity createKnowledgeArea(@RequestBody @Valid KnowledgeAreaRequestDTO knowledgeArea) throws URISyntaxException { log.debug("REST request to create knowledge area : {}", knowledgeArea); @@ -134,8 +128,6 @@ public ResponseEntity createKnowledgeArea(@RequestBody @ * @return the ResponseEntity with status 200 (OK) and with body the updated knowledge area */ @PutMapping("standardized-competencies/knowledge-areas/{knowledgeAreaId}") - @FeatureToggle(Feature.StandardizedCompetencies) - @EnforceAdmin public ResponseEntity updateKnowledgeArea(@PathVariable long knowledgeAreaId, @RequestBody @Valid KnowledgeAreaRequestDTO knowledgeArea) { log.debug("REST request to update knowledge area : {}", knowledgeArea); @@ -151,8 +143,6 @@ public ResponseEntity updateKnowledgeArea(@PathVariable * @return the ResponseEntity with status 200 (OK) */ @DeleteMapping("standardized-competencies/knowledge-areas/{knowledgeAreaId}") - @FeatureToggle(Feature.StandardizedCompetencies) - @EnforceAdmin public ResponseEntity deleteKnowledgeArea(@PathVariable long knowledgeAreaId) { log.debug("REST request to delete knowledge area : {}", knowledgeAreaId); @@ -168,8 +158,6 @@ public ResponseEntity deleteKnowledgeArea(@PathVariable long knowledgeArea * @return the ResponseEntity with status 200 (OK) */ @PutMapping("standardized-competencies/import") - @FeatureToggle(Feature.StandardizedCompetencies) - @EnforceAdmin public ResponseEntity importStandardizedCompetencyCatalog(@RequestBody @Valid StandardizedCompetencyCatalogDTO standardizedCompetencyCatalogDTO) { log.debug("REST request to import standardized competency catalog"); @@ -184,8 +172,6 @@ public ResponseEntity importStandardizedCompetencyCatalog(@RequestBody @Va * @return the ResponseEntity with status 200 (OK) and the body containing the JSON string of the standardized competency catalog */ @GetMapping("standardized-competencies/export") - @FeatureToggle(Feature.StandardizedCompetencies) - @EnforceAdmin public ResponseEntity exportStandardizedCompetencyCatalog() { log.debug("REST request to export standardized competency catalog"); diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildConfig.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildConfig.java index bce9a9b20e65..9a41fc6fdc20 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildConfig.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildConfig.java @@ -15,7 +15,7 @@ @JsonInclude(JsonInclude.Include.NON_EMPTY) public record BuildConfig(String buildScript, String dockerImage, String commitHashToBuild, String assignmentCommitHash, String testCommitHash, String branch, ProgrammingLanguage programmingLanguage, ProjectType projectType, boolean scaEnabled, boolean sequentialTestRunsEnabled, boolean testwiseCoverageEnabled, - List resultPaths) implements Serializable { + List resultPaths, int timeoutSeconds, String assignmentCheckoutPath, String testCheckoutPath, String solutionCheckoutPath) implements Serializable { @Override public String dockerImage() { 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 8df60f45026f..b7c97daa9786 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 @@ -24,6 +24,7 @@ import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -275,11 +276,22 @@ public String getIDOfRunningContainer(String containerName) { * @param auxiliaryRepositoriesPaths An array of paths for auxiliary repositories to be included in the build process. * @param auxiliaryRepositoryCheckoutDirectories An array of directory names within the container where each auxiliary repository should be checked out. * @param programmingLanguage The programming language of the repositories, which influences directory naming conventions. + * @param assignmentCheckoutPath The directory within the container where the assignment repository should be checked out; can be null if not applicable, + * default would be used. + * @param testCheckoutPath The directory within the container where the test repository should be checked out; can be null if not applicable, default + * would be used. + * @param solutionCheckoutPath The directory within the container where the solution repository should be checked out; can be null if not applicable, default + * would be used. */ public void populateBuildJobContainer(String buildJobContainerId, Path assignmentRepositoryPath, Path testRepositoryPath, Path solutionRepositoryPath, - Path[] auxiliaryRepositoriesPaths, String[] auxiliaryRepositoryCheckoutDirectories, ProgrammingLanguage programmingLanguage) { - String testCheckoutPath = RepositoryCheckoutPath.TEST.forProgrammingLanguage(programmingLanguage); - String assignmentCheckoutPath = RepositoryCheckoutPath.ASSIGNMENT.forProgrammingLanguage(programmingLanguage); + Path[] auxiliaryRepositoriesPaths, String[] auxiliaryRepositoryCheckoutDirectories, ProgrammingLanguage programmingLanguage, String assignmentCheckoutPath, + String testCheckoutPath, String solutionCheckoutPath) { + + assignmentCheckoutPath = (!StringUtils.isBlank(assignmentCheckoutPath)) ? assignmentCheckoutPath + : RepositoryCheckoutPath.ASSIGNMENT.forProgrammingLanguage(programmingLanguage); + + String defaultTestCheckoutPath = RepositoryCheckoutPath.TEST.forProgrammingLanguage(programmingLanguage); + testCheckoutPath = (!StringUtils.isBlank(defaultTestCheckoutPath) && !StringUtils.isBlank(testCheckoutPath)) ? testCheckoutPath : defaultTestCheckoutPath; // Make sure to create the working directory in case it does not exist. // In case the test checkout path is the working directory, we only create up to the parent, as the working directory is created below. @@ -292,7 +304,8 @@ public void populateBuildJobContainer(String buildJobContainerId, Path assignmen // Copy the assignment repository to the container and move it to the assignment checkout path addAndPrepareDirectory(buildJobContainerId, assignmentRepositoryPath, LOCALCI_WORKING_DIRECTORY + "/testing-dir/" + assignmentCheckoutPath); if (solutionRepositoryPath != null) { - String solutionCheckoutPath = RepositoryCheckoutPath.SOLUTION.forProgrammingLanguage(programmingLanguage); + solutionCheckoutPath = (!StringUtils.isBlank(solutionCheckoutPath)) ? solutionCheckoutPath + : RepositoryCheckoutPath.SOLUTION.forProgrammingLanguage(programmingLanguage); addAndPrepareDirectory(buildJobContainerId, solutionRepositoryPath, LOCALCI_WORKING_DIRECTORY + "/testing-dir/" + solutionCheckoutPath); } for (int i = 0; i < auxiliaryRepositoriesPaths.length; i++) { @@ -309,6 +322,7 @@ private void createScriptFile(String buildJobContainerId) { private void addAndPrepareDirectory(String containerId, Path repositoryPath, String newDirectoryName) { copyToContainer(repositoryPath.toString(), containerId); + addDirectory(containerId, getParentFolderPath(newDirectoryName), true); renameDirectoryOrFile(containerId, LOCALCI_WORKING_DIRECTORY + "/" + repositoryPath.getFileName().toString(), newDirectoryName); } @@ -428,4 +442,9 @@ private Container getContainerForName(String containerName) { List containers = dockerClient.listContainersCmd().withShowAll(true).exec(); return containers.stream().filter(container -> container.getNames()[0].equals("/" + containerName)).findFirst().orElse(null); } + + private String getParentFolderPath(String path) { + Path parentPath = Paths.get(path).normalize().getParent(); + return parentPath != null ? parentPath.toString() : ""; + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobExecutionService.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobExecutionService.java index f25d5b6f749d..9c968c453e47 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobExecutionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobExecutionService.java @@ -245,7 +245,8 @@ private BuildResult runScriptAndParseResults(BuildJobQueueItem buildJob, String buildLogsMap.appendBuildLogEntry(buildJob.id(), msg); log.debug(msg); buildJobContainerService.populateBuildJobContainer(containerId, assignmentRepositoryPath, testsRepositoryPath, solutionRepositoryPath, auxiliaryRepositoriesPaths, - buildJob.repositoryInfo().auxiliaryRepositoryCheckoutDirectories(), buildJob.buildConfig().programmingLanguage()); + buildJob.repositoryInfo().auxiliaryRepositoryCheckoutDirectories(), buildJob.buildConfig().programmingLanguage(), buildJob.buildConfig().assignmentCheckoutPath(), + buildJob.buildConfig().testCheckoutPath(), buildJob.buildConfig().solutionCheckoutPath()); msg = "~~~~~~~~~~~~~~~~~~~~ Executing Build Script for Build job " + buildJob.id() + " ~~~~~~~~~~~~~~~~~~~~"; buildLogsMap.appendBuildLogEntry(buildJob.id(), msg); diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobManagementService.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobManagementService.java index f514d59496cd..804d7d1e50df 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobManagementService.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobManagementService.java @@ -58,7 +58,7 @@ public class BuildJobManagementService { private final ReentrantLock lock = new ReentrantLock(); - @Value("${artemis.continuous-integration.timeout-seconds:240}") + @Value("${artemis.continuous-integration.timeout-seconds:120}") private int timeoutSeconds; @Value("${artemis.continuous-integration.asynchronous:true}") @@ -149,9 +149,17 @@ public CompletableFuture executeBuildJob(BuildJobQueueItem buildJob lock.unlock(); } + int buildJobTimeoutSeconds; + if (buildJobItem.buildConfig().timeoutSeconds() != 0 && buildJobItem.buildConfig().timeoutSeconds() < this.timeoutSeconds) { + buildJobTimeoutSeconds = buildJobItem.buildConfig().timeoutSeconds(); + } + else { + buildJobTimeoutSeconds = this.timeoutSeconds; + } + CompletableFuture futureResult = createCompletableFuture(() -> { try { - return future.get(timeoutSeconds, TimeUnit.SECONDS); + return future.get(buildJobTimeoutSeconds, TimeUnit.SECONDS); } catch (Exception e) { // RejectedExecutionException is thrown if the queue size limit (defined in "artemis.continuous-integration.queue-size-limit") is reached. diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/SharedQueueProcessingService.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/SharedQueueProcessingService.java index f82f7aa7a36f..7534de04e3bf 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/SharedQueueProcessingService.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/SharedQueueProcessingService.java @@ -294,6 +294,7 @@ private void processBuild(BuildJobQueueItem buildJob) { CompletableFuture futureResult = buildJobManagementService.executeBuildJob(buildJob); futureResult.thenAccept(buildResult -> { + log.debug("Build job completed: {}", buildJob); JobTimingInfo jobTimingInfo = new JobTimingInfo(buildJob.jobTimingInfo().submissionDate(), buildJob.jobTimingInfo().buildStartDate(), ZonedDateTime.now()); BuildJobQueueItem finishedJob = new BuildJobQueueItem(buildJob.id(), buildJob.name(), buildJob.buildAgentAddress(), buildJob.participationId(), buildJob.courseId(), @@ -316,6 +317,8 @@ private void processBuild(BuildJobQueueItem buildJob) { }); futureResult.exceptionally(ex -> { + log.debug("Build job completed with exception: {}", buildJob, ex); + ZonedDateTime completionDate = ZonedDateTime.now(); BuildJobQueueItem job; @@ -364,6 +367,7 @@ public class QueuedBuildJobItemListener implements ItemListener event) { log.debug("CIBuildJobQueueItem added to queue: {}", event.getItem()); + log.debug("Current queued items: {}", queue.size()); checkAvailabilityAndProcessNextBuild(); } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/domain/Faq.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/Faq.java new file mode 100644 index 000000000000..fd7ce4fca468 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/Faq.java @@ -0,0 +1,99 @@ +package de.tum.cit.aet.artemis.communication.domain; + +import java.util.HashSet; +import java.util.Set; + +import jakarta.persistence.CollectionTable; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.cit.aet.artemis.core.domain.AbstractAuditingEntity; +import de.tum.cit.aet.artemis.core.domain.Course; + +/** + * A FAQ. + */ +@Entity +@Table(name = "faq") +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class Faq extends AbstractAuditingEntity { + + @Column(name = "question_title") + private String questionTitle; + + @Column(name = "question_answer") + private String questionAnswer; + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "faq_category", joinColumns = @JoinColumn(name = "faq_id")) + @Column(name = "category") + private Set categories = new HashSet<>(); + + @Enumerated(EnumType.STRING) + @Column(name = "faq_state") + private FaqState faqState; + + @ManyToOne + @JsonIgnoreProperties(value = { "faqs" }, allowSetters = true) + private Course course; + + public String getQuestionTitle() { + return questionTitle; + } + + public void setQuestionTitle(String questionTitle) { + this.questionTitle = questionTitle; + } + + public String getQuestionAnswer() { + return questionAnswer; + } + + public void setQuestionAnswer(String questionAnswer) { + this.questionAnswer = questionAnswer; + } + + public Course getCourse() { + return course; + } + + public void setCourse(Course course) { + this.course = course; + } + + public Set getCategories() { + return categories; + } + + public void setCategories(Set categories) { + this.categories = categories; + } + + public FaqState getFaqState() { + return faqState; + } + + public void setFaqState(FaqState faqState) { + this.faqState = faqState; + } + + @Override + public String toString() { + return "Faq{" + "id=" + getId() + ", questionTitle='" + getQuestionTitle() + "'" + ", questionAnswer='" + getQuestionAnswer() + "'" + ", faqState='" + getFaqState() + "}"; + } + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/domain/FaqState.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/FaqState.java new file mode 100644 index 000000000000..9018a3be3a12 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/FaqState.java @@ -0,0 +1,5 @@ +package de.tum.cit.aet.artemis.communication.domain; + +public enum FaqState { + ACCEPTED, REJECTED, PROPOSED +} diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/domain/Post.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/Post.java index d3bf4e217ea3..4ff2d48fedf5 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/domain/Post.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/Post.java @@ -167,6 +167,10 @@ public void addTag(String tag) { this.tags.add(tag); } + public void setCourse(Course course) { + this.course = course; + } + public Conversation getConversation() { return conversation; } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java index f143f959f89f..71dc52ae4e93 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java @@ -434,7 +434,7 @@ public static String[] createPlaceholdersForUserGroupChat(String courseTitle, St } @NotificationPlaceholderCreator(values = { CONVERSATION_ADD_USER_CHANNEL, CONVERSATION_REMOVE_USER_CHANNEL, CONVERSATION_DELETE_CHANNEL }) - public static String[] createPlaceholdersForUserChannel(String courseTitle, String channelName, String responsibleForUserName) { - return new String[] { courseTitle, channelName, responsibleForUserName }; + public static String[] createPlaceholdersForUserChannel(String courseTitle, String conversationName, String responsibleForUserName) { + return new String[] { courseTitle, conversationName, responsibleForUserName }; } } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/domain/push_notification/PushNotificationDeviceConfiguration.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/push_notification/PushNotificationDeviceConfiguration.java index c6bfd3384110..b9a911ce6194 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/domain/push_notification/PushNotificationDeviceConfiguration.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/push_notification/PushNotificationDeviceConfiguration.java @@ -106,7 +106,9 @@ public boolean equals(Object object) { return false; } PushNotificationDeviceConfiguration that = (PushNotificationDeviceConfiguration) object; - return token.equals(that.token) && deviceType == that.deviceType && expirationDate.equals(that.expirationDate) && Arrays.equals(secretKey, that.secretKey) + // Use compareTo rather than equals for dates to ensure timestamps and dates with the same time are considered equal + // This is caused by Java internal design having different classes for Date (java.util) and Timestamp (java.sql) + return token.equals(that.token) && deviceType == that.deviceType && expirationDate.compareTo(that.expirationDate) == 0 && Arrays.equals(secretKey, that.secretKey) && owner.equals(that.owner); } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/dto/FaqDTO.java b/src/main/java/de/tum/cit/aet/artemis/communication/dto/FaqDTO.java new file mode 100644 index 000000000000..efab02ad1bf9 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/communication/dto/FaqDTO.java @@ -0,0 +1,17 @@ +package de.tum.cit.aet.artemis.communication.dto; + +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.cit.aet.artemis.communication.domain.Faq; +import de.tum.cit.aet.artemis.communication.domain.FaqState; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record FaqDTO(Long id, String questionTitle, String questionAnswer, Set categories, FaqState faqState) { + + public FaqDTO(Faq faq) { + this(faq.getId(), faq.getQuestionTitle(), faq.getQuestionAnswer(), faq.getCategories(), faq.getFaqState()); + } + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/repository/AnswerPostRepository.java b/src/main/java/de/tum/cit/aet/artemis/communication/repository/AnswerPostRepository.java index c6a915b994e2..db61138b3a73 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/repository/AnswerPostRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/repository/AnswerPostRepository.java @@ -30,4 +30,6 @@ default AnswerPost findAnswerPostByIdElseThrow(Long answerPostId) { default AnswerPost findAnswerMessageByIdElseThrow(Long answerPostId) { return getValueElseThrow(findById(answerPostId).filter(answerPost -> answerPost.getPost().getConversation() != null), answerPostId); } + + long countAnswerPostsByPostIdIn(List postIds); } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/repository/FaqRepository.java b/src/main/java/de/tum/cit/aet/artemis/communication/repository/FaqRepository.java new file mode 100644 index 000000000000..bd8bb8989995 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/communication/repository/FaqRepository.java @@ -0,0 +1,37 @@ +package de.tum.cit.aet.artemis.communication.repository; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + +import java.util.Set; + +import org.springframework.context.annotation.Profile; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import de.tum.cit.aet.artemis.communication.domain.Faq; +import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; + +/** + * Spring Data repository for the Faq entity. + */ +@Profile(PROFILE_CORE) +@Repository +public interface FaqRepository extends ArtemisJpaRepository { + + Set findAllByCourseId(Long courseId); + + @Query(""" + SELECT DISTINCT faq.categories + FROM Faq faq + WHERE faq.course.id = :courseId + """) + Set findAllCategoriesByCourseId(@Param("courseId") Long courseId); + + @Transactional + @Modifying + void deleteAllByCourseId(Long courseId); + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/repository/PostRepository.java b/src/main/java/de/tum/cit/aet/artemis/communication/repository/PostRepository.java index 449a629fb4af..aacfbc33d179 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/repository/PostRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/repository/PostRepository.java @@ -45,4 +45,8 @@ default Post findPostByIdElseThrow(Long postId) throws EntityNotFoundException { default Post findPostOrMessagePostByIdElseThrow(Long postId) throws EntityNotFoundException { return getValueElseThrow(findById(postId), postId); } + + List findAllByConversationId(Long conversationId); + + List findAllByCourseId(Long courseId); } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/ConversationNotificationService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/ConversationNotificationService.java index 1e13dce59c5a..d009074927b2 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/ConversationNotificationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/ConversationNotificationService.java @@ -18,6 +18,7 @@ import de.tum.cit.aet.artemis.communication.domain.conversation.Channel; import de.tum.cit.aet.artemis.communication.domain.conversation.Conversation; import de.tum.cit.aet.artemis.communication.domain.conversation.GroupChat; +import de.tum.cit.aet.artemis.communication.domain.conversation.OneToOneChat; import de.tum.cit.aet.artemis.communication.domain.notification.ConversationNotification; import de.tum.cit.aet.artemis.communication.domain.notification.NotificationPlaceholderCreator; import de.tum.cit.aet.artemis.communication.domain.notification.SingleUserNotification; @@ -57,37 +58,39 @@ public ConversationNotificationService(ConversationNotificationRepository conver * @return the created notification */ public ConversationNotification createNotification(Post createdMessage, Conversation conversation, Course course, Set mentionedUsers) { - String notificationText; - String[] placeholders; NotificationType notificationType = CONVERSATION_NEW_MESSAGE; String conversationName = conversation.getHumanReadableNameForReceiver(createdMessage.getAuthor()); + String conversationType; + String notificationText; // add channel/groupChat/oneToOneChat string to placeholders for notification to distinguish in mobile client - if (conversation instanceof Channel channel) { - notificationText = NEW_MESSAGE_CHANNEL_TEXT; - placeholders = createPlaceholdersNewMessageChannelText(course.getTitle(), createdMessage.getContent(), createdMessage.getCreationDate().toString(), - createdMessage.getAuthor().getName(), conversationName, "channel"); - notificationType = getNotificationTypeForChannel(channel); - } - else if (conversation instanceof GroupChat) { - notificationText = NEW_MESSAGE_GROUP_CHAT_TEXT; - placeholders = createPlaceholdersNewMessageChannelText(course.getTitle(), createdMessage.getContent(), createdMessage.getCreationDate().toString(), - createdMessage.getAuthor().getName(), conversationName, "groupChat"); - } - else { - notificationText = NEW_MESSAGE_DIRECT_TEXT; - placeholders = createPlaceholdersNewMessageChannelText(course.getTitle(), createdMessage.getContent(), createdMessage.getCreationDate().toString(), - createdMessage.getAuthor().getName(), conversationName, "oneToOneChat"); + switch (conversation) { + case Channel channel -> { + notificationText = NEW_MESSAGE_CHANNEL_TEXT; + conversationType = "channel"; + notificationType = getNotificationTypeForChannel(channel); + } + case GroupChat ignored -> { + notificationText = NEW_MESSAGE_GROUP_CHAT_TEXT; + conversationType = "groupChat"; + } + case OneToOneChat ignored -> { + notificationText = NEW_MESSAGE_DIRECT_TEXT; + conversationType = "oneToOneChat"; + } + default -> throw new IllegalStateException("Unexpected value: " + conversation); } + String[] placeholders = createPlaceholdersNewMessageChannelText(course.getTitle(), createdMessage.getContent(), createdMessage.getCreationDate().toString(), + conversationName, createdMessage.getAuthor().getName(), conversationType); ConversationNotification notification = createConversationMessageNotification(course.getId(), createdMessage, notificationType, notificationText, true, placeholders); save(notification, mentionedUsers, placeholders); return notification; } @NotificationPlaceholderCreator(values = { CONVERSATION_NEW_MESSAGE }) - public static String[] createPlaceholdersNewMessageChannelText(String courseTitle, String messageContent, String messageCreationDate, String channelName, String authorName, - String conversationType) { - return new String[] { courseTitle, messageContent, messageCreationDate, channelName, authorName, conversationType }; + public static String[] createPlaceholdersNewMessageChannelText(String courseTitle, String messageContent, String messageCreationDate, String conversationName, + String authorName, String conversationType) { + return new String[] { courseTitle, messageContent, messageCreationDate, conversationName, authorName, conversationType }; } private void save(ConversationNotification notification, Set mentionedUsers, String[] placeHolders) { diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java b/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java new file mode 100644 index 000000000000..91a542aaa220 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java @@ -0,0 +1,194 @@ +package de.tum.cit.aet.artemis.communication.web; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import de.tum.cit.aet.artemis.communication.domain.Faq; +import de.tum.cit.aet.artemis.communication.dto.FaqDTO; +import de.tum.cit.aet.artemis.communication.repository.FaqRepository; +import de.tum.cit.aet.artemis.core.domain.Course; +import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; +import de.tum.cit.aet.artemis.core.repository.CourseRepository; +import de.tum.cit.aet.artemis.core.security.Role; +import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastInstructor; +import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; +import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; +import de.tum.cit.aet.artemis.core.util.HeaderUtil; + +/** + * REST controller for managing Faqs. + */ +@Profile(PROFILE_CORE) +@RestController +@RequestMapping("api/") +public class FaqResource { + + private static final Logger log = LoggerFactory.getLogger(FaqResource.class); + + private static final String ENTITY_NAME = "faq"; + + @Value("${jhipster.clientApp.name}") + private String applicationName; + + private final CourseRepository courseRepository; + + private final AuthorizationCheckService authCheckService; + + private final FaqRepository faqRepository; + + public FaqResource(CourseRepository courseRepository, AuthorizationCheckService authCheckService, FaqRepository faqRepository) { + + this.courseRepository = courseRepository; + this.authCheckService = authCheckService; + this.faqRepository = faqRepository; + } + + /** + * POST /courses/:courseId/faqs : Create a new faq. + * + * @param faq the faq to create * + * @param courseId the id of the course the faq belongs to + * @return the ResponseEntity with status 201 (Created) and with body the new faq, or with status 400 (Bad Request) + * if the faq has already an ID or if the faq course id does not match with the path variable + * @throws URISyntaxException if the Location URI syntax is incorrect + */ + @PostMapping("courses/{courseId}/faqs") + @EnforceAtLeastInstructor + public ResponseEntity createFaq(@RequestBody Faq faq, @PathVariable Long courseId) throws URISyntaxException { + log.debug("REST request to save Faq : {}", faq); + if (faq.getId() != null) { + throw new BadRequestAlertException("A new faq cannot already have an ID", ENTITY_NAME, "idExists"); + } + + if (faq.getCourse() == null || !faq.getCourse().getId().equals(courseId)) { + throw new BadRequestAlertException("Course ID in path and FAQ do not match", ENTITY_NAME, "courseIdMismatch"); + } + authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, faq.getCourse(), null); + + Faq savedFaq = faqRepository.save(faq); + FaqDTO dto = new FaqDTO(savedFaq); + return ResponseEntity.created(new URI("/api/courses/" + courseId + "/faqs/" + savedFaq.getId())).body(dto); + } + + /** + * PUT /courses/:courseId/faqs/:faqId : Updates an existing faq. + * + * @param faq the faq to update + * @param faqId id of the faq to be updated * + * @param courseId the id of the course the faq belongs to + * @return the ResponseEntity with status 200 (OK) and with body the updated faq, or with status 400 (Bad Request) + * if the faq is not valid or if the faq course id does not match with the path variable + */ + @PutMapping("courses/{courseId}/faqs/{faqId}") + @EnforceAtLeastInstructor + public ResponseEntity updateFaq(@RequestBody Faq faq, @PathVariable Long faqId, @PathVariable Long courseId) { + log.debug("REST request to update Faq : {}", faq); + if (faqId == null || !faqId.equals(faq.getId())) { + throw new BadRequestAlertException("Id of FAQ and path must match", ENTITY_NAME, "idNull"); + } + authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, faq.getCourse(), null); + Faq existingFaq = faqRepository.findByIdElseThrow(faqId); + if (!Objects.equals(existingFaq.getCourse().getId(), courseId)) { + throw new BadRequestAlertException("Course ID of the FAQ provided courseID must match", ENTITY_NAME, "idNull"); + } + Faq updatedFaq = faqRepository.save(faq); + FaqDTO dto = new FaqDTO(updatedFaq); + return ResponseEntity.ok().body(dto); + } + + /** + * GET /courses/:courseId/faqs/:faqId : get the faq with the id faqId. + * + * @param faqId the faqId of the faq to retrieve * + * @param courseId the id of the course the faq belongs to + * @return the ResponseEntity with status 200 (OK) and with body the faq, or with status 404 (Not Found) + */ + @GetMapping("courses/{courseId}/faqs/{faqId}") + @EnforceAtLeastStudent + public ResponseEntity getFaq(@PathVariable Long faqId, @PathVariable Long courseId) { + log.debug("REST request to get faq {}", faqId); + Faq faq = faqRepository.findByIdElseThrow(faqId); + if (faq.getCourse() == null || !faq.getCourse().getId().equals(courseId)) { + throw new BadRequestAlertException("Course ID in path and FAQ do not match", ENTITY_NAME, "courseIdMismatch"); + } + authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, faq.getCourse(), null); + FaqDTO dto = new FaqDTO(faq); + return ResponseEntity.ok(dto); + } + + /** + * DELETE /courses/:courseId/faqs/:faqId : delete the "id" faq. + * + * @param faqId the id of the faq to delete + * @param courseId the id of the course the faq belongs to + * @return the ResponseEntity with status 200 (OK) + */ + @DeleteMapping("courses/{courseId}/faqs/{faqId}") + @EnforceAtLeastInstructor + public ResponseEntity deleteFaq(@PathVariable Long faqId, @PathVariable Long courseId) { + + log.debug("REST request to delete faq {}", faqId); + Faq existingFaq = faqRepository.findByIdElseThrow(faqId); + authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, existingFaq.getCourse(), null); + if (!Objects.equals(existingFaq.getCourse().getId(), courseId)) { + throw new BadRequestAlertException("Course ID of the FAQ provided courseID must match", ENTITY_NAME, "idNull"); + } + faqRepository.deleteById(faqId); + return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, faqId.toString())).build(); + } + + /** + * GET /courses/:courseId/faqs : get all the faqs of a course + * + * @param courseId the courseId of the course for which all faqs should be returned + * @return the ResponseEntity with status 200 (OK) and the list of faqs in body + */ + @GetMapping("courses/{courseId}/faqs") + @EnforceAtLeastStudent + public ResponseEntity> getFaqForCourse(@PathVariable Long courseId) { + log.debug("REST request to get all Faqs for the course with id : {}", courseId); + + Course course = courseRepository.findByIdElseThrow(courseId); + authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, null); + Set faqs = faqRepository.findAllByCourseId(courseId); + Set faqDTOS = faqs.stream().map(FaqDTO::new).collect(Collectors.toSet()); + return ResponseEntity.ok().body(faqDTOS); + } + + /** + * GET /courses/:courseId/faq-categories : get all the faq categories of a course + * + * @param courseId the courseId of the course for which all faq categories should be returned + * @return the ResponseEntity with status 200 (OK) and the list of faqs in body + */ + @GetMapping("courses/{courseId}/faq-categories") + @EnforceAtLeastStudent + public ResponseEntity> getFaqCategoriesForCourse(@PathVariable Long courseId) { + log.debug("REST request to get all Faq Categories for the course with id : {}", courseId); + + Course course = courseRepository.findByIdElseThrow(courseId); + authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, null); + Set faqs = faqRepository.findAllCategoriesByCourseId(courseId); + + return ResponseEntity.ok().body(faqs); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/web/admin/AdminSystemNotificationResource.java b/src/main/java/de/tum/cit/aet/artemis/communication/web/admin/AdminSystemNotificationResource.java index 6850598633e9..4e94766284d6 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/web/admin/AdminSystemNotificationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/web/admin/AdminSystemNotificationResource.java @@ -30,6 +30,7 @@ * REST controller for administrating system notifications. */ @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminSystemNotificationResource { @@ -58,7 +59,6 @@ public AdminSystemNotificationResource(SystemNotificationRepository systemNotifi * @throws URISyntaxException if the Location URI syntax is incorrect */ @PostMapping("system-notifications") - @EnforceAdmin public ResponseEntity createSystemNotification(@RequestBody SystemNotification systemNotification) throws URISyntaxException { log.debug("REST request to save SystemNotification : {}", systemNotification); if (systemNotification.getId() != null) { @@ -79,7 +79,6 @@ public ResponseEntity createSystemNotification(@RequestBody System * status 500 (Internal Server Error) if the system notification couldn't be updated */ @PutMapping("system-notifications") - @EnforceAdmin public ResponseEntity updateSystemNotification(@RequestBody SystemNotification systemNotification) { log.debug("REST request to update SystemNotification : {}", systemNotification); if (systemNotification.getId() == null) { @@ -101,7 +100,6 @@ public ResponseEntity updateSystemNotification(@RequestBody * @return the ResponseEntity with status 200 (OK) */ @DeleteMapping("system-notifications/{notificationId}") - @EnforceAdmin public ResponseEntity deleteSystemNotification(@PathVariable Long notificationId) { log.debug("REST request to delete SystemNotification : {}", notificationId); systemNotificationRepository.deleteById(notificationId); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/Constants.java b/src/main/java/de/tum/cit/aet/artemis/core/config/Constants.java index 71e4dc0a5775..3f61f7de5ff0 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/config/Constants.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/Constants.java @@ -93,6 +93,8 @@ public final class Constants { // Used to cut off CI specific path segments when receiving static code analysis reports public static final String ASSIGNMENT_DIRECTORY = "/" + ASSIGNMENT_REPO_NAME + "/"; + public static final String TEST_WORKING_DIRECTORY = "test"; + // Used as a value for for the Java template pom.xml public static final String STUDENT_WORKING_DIRECTORY = ASSIGNMENT_DIRECTORY + "src"; @@ -390,6 +392,18 @@ public final class Constants { */ public static final int MIN_SCORE_ORANGE = 40; + public static final String ASSIGNMENT_REPO_PLACEHOLDER = "${studentWorkingDirectory}"; + + public static final String TEST_REPO_PLACEHOLDER = "${testWorkingDirectory}"; + + public static final String SOLUTION_REPO_PLACEHOLDER = "${solutionWorkingDirectory}"; + + public static final String ASSIGNMENT_REPO_PARENT_PLACEHOLDER = "${studentParentWorkingDirectoryName}"; + + public static final String ASSIGNMENT_REPO_PLACEHOLDER_NO_SLASH = "${studentWorkingDirectoryNoSlash}"; + + public static final Pattern ALLOWED_CHECKOUT_DIRECTORY = Pattern.compile("[\\w-]+(/[\\w-]+)*$"); + private Constants() { } } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java b/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java index 9163cfb7d7f1..d0c6941cc698 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java @@ -64,7 +64,6 @@ import de.tum.cit.aet.artemis.core.security.jwt.TokenProvider; import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; import de.tum.cit.aet.artemis.exam.repository.ExamRepository; -import de.tum.cit.aet.artemis.exercise.domain.Exercise; import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation; import de.tum.cit.aet.artemis.exercise.repository.ExerciseRepository; import de.tum.cit.aet.artemis.exercise.repository.StudentParticipationRepository; @@ -309,8 +308,7 @@ private boolean allowSubscription(@Nullable Principal principal, String destinat // TODO: Is it right that TAs are not allowed to subscribe to exam exercises? if (exerciseRepository.isExamExercise(exerciseId)) { - Exercise exercise = exerciseRepository.findByIdElseThrow(exerciseId); - return authorizationCheckService.isAtLeastInstructorInCourse(login, exercise.getCourseViaExerciseGroupOrCourseMember().getId()); + return authorizationCheckService.isAtLeastInstructorInExercise(login, exerciseId); } else { return authorizationCheckService.isAtLeastTeachingAssistantInExercise(login, exerciseId); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/domain/Course.java b/src/main/java/de/tum/cit/aet/artemis/core/domain/Course.java index beacc4af0aa0..8000a24c0b55 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/domain/Course.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/domain/Course.java @@ -37,6 +37,7 @@ import de.tum.cit.aet.artemis.atlas.domain.competency.Competency; import de.tum.cit.aet.artemis.atlas.domain.competency.LearningPath; import de.tum.cit.aet.artemis.atlas.domain.competency.Prerequisite; +import de.tum.cit.aet.artemis.communication.domain.Faq; import de.tum.cit.aet.artemis.communication.domain.Post; import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; import de.tum.cit.aet.artemis.exam.domain.Exam; @@ -187,6 +188,9 @@ public class Course extends DomainObject { @Column(name = "unenrollment_enabled") private boolean unenrollmentEnabled = false; + @Column(name = "faq_enabled") + private boolean faqEnabled = false; + @Column(name = "presentation_score") private Integer presentationScore; @@ -260,6 +264,10 @@ public class Course extends DomainObject { @JsonIgnoreProperties("course") private TutorialGroupsConfiguration tutorialGroupsConfiguration; + @OneToMany(mappedBy = "course", cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = "course", allowSetters = true) + private Set faqs = new HashSet<>(); + // NOTE: Helpers variable names must be different from Getter name, so that Jackson ignores the @Transient annotation, but Hibernate still respects it @Transient private Long numberOfInstructorsTransient; @@ -627,6 +635,14 @@ public void setEnrollmentEnabled(Boolean enrollmentEnabled) { this.enrollmentEnabled = enrollmentEnabled; } + public boolean isFaqEnabled() { + return faqEnabled; + } + + public void setFaqEnabled(boolean faqEnabled) { + this.faqEnabled = faqEnabled; + } + public String getEnrollmentConfirmationMessage() { return enrollmentConfirmationMessage; } @@ -717,7 +733,7 @@ public String toString() { + "'" + ", enrollmentStartDate='" + getEnrollmentStartDate() + "'" + ", enrollmentEndDate='" + getEnrollmentEndDate() + "'" + ", unenrollmentEndDate='" + getUnenrollmentEndDate() + "'" + ", semester='" + getSemester() + "'" + "'" + ", onlineCourse='" + isOnlineCourse() + "'" + ", color='" + getColor() + "'" + ", courseIcon='" + getCourseIcon() + "'" + ", enrollmentEnabled='" + isEnrollmentEnabled() + "'" + ", unenrollmentEnabled='" + isUnenrollmentEnabled() + "'" - + ", presentationScore='" + getPresentationScore() + "'" + "}"; + + ", presentationScore='" + getPresentationScore() + "'" + ", faqEnabled='" + isFaqEnabled() + "'" + "}"; } public void setNumberOfInstructors(Long numberOfInstructors) { @@ -1057,4 +1073,17 @@ public String getMappedColumnName() { return mappedColumnName; } } + + public Set getFaqs() { + return faqs; + } + + public void setFaqs(Set faqs) { + this.faqs = faqs; + } + + public void addFaq(Faq faq) { + this.faqs.add(faq); + faq.setCourse(this); + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/domain/User.java b/src/main/java/de/tum/cit/aet/artemis/core/domain/User.java index 7fa995658af1..6498340f3bc2 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/domain/User.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/domain/User.java @@ -561,4 +561,9 @@ public void hasAcceptedIrisElseThrow() { public String getSshPublicKey() { return sshPublicKey; } + + @Nullable + public @Size(max = 100) String getSshPublicKeyHash() { + return sshPublicKeyHash; + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/dto/CourseDeletionSummaryDTO.java b/src/main/java/de/tum/cit/aet/artemis/core/dto/CourseDeletionSummaryDTO.java new file mode 100644 index 000000000000..2f3b8d51596c --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/core/dto/CourseDeletionSummaryDTO.java @@ -0,0 +1,7 @@ +package de.tum.cit.aet.artemis.core.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record CourseDeletionSummaryDTO(long numberOfBuilds, long numberOfCommunicationPosts, long numberOfAnswerPosts) { +} diff --git a/src/main/java/de/tum/cit/aet/artemis/core/dto/UserDTO.java b/src/main/java/de/tum/cit/aet/artemis/core/dto/UserDTO.java index 1ac72940f919..3d627425a8f7 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/dto/UserDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/dto/UserDTO.java @@ -78,6 +78,8 @@ public class UserDTO extends AuditingEntityDTO { private String sshPublicKey; + private String sshKeyHash; + private ZonedDateTime irisAccepted; public UserDTO() { @@ -291,4 +293,12 @@ public ZonedDateTime getIrisAccepted() { public void setIrisAccepted(ZonedDateTime irisAccepted) { this.irisAccepted = irisAccepted; } + + public String getSshKeyHash() { + return sshKeyHash; + } + + public void setSshKeyHash(String sshKeyHash) { + this.sshKeyHash = sshKeyHash; + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/dto/UserPublicInfoDTO.java b/src/main/java/de/tum/cit/aet/artemis/core/dto/UserPublicInfoDTO.java index a6da8966dfc5..f84bf9e0819a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/dto/UserPublicInfoDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/dto/UserPublicInfoDTO.java @@ -25,6 +25,8 @@ public class UserPublicInfoDTO { private String lastName; + private String imageUrl; + private Boolean isInstructor; private Boolean isEditor; @@ -43,6 +45,7 @@ public UserPublicInfoDTO(User user) { this.name = user.getName(); this.firstName = user.getFirstName(); this.lastName = user.getLastName(); + this.imageUrl = user.getImageUrl(); } /** @@ -101,6 +104,14 @@ public void setLastName(String lastName) { this.lastName = lastName; } + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + public Boolean getIsInstructor() { return isInstructor; } @@ -152,6 +163,7 @@ public int hashCode() { @Override public String toString() { return "UserPublicInfoDTO{" + "id=" + id + ", login='" + login + '\'' + ", name='" + name + '\'' + ", firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' - + ", isInstructor=" + isInstructor + ", isEditor=" + isEditor + ", isTeachingAssistant=" + isTeachingAssistant + ", isStudent=" + isStudent + '}'; + + ", imageUrl='" + imageUrl + '\'' + ", isInstructor=" + isInstructor + ", isEditor=" + isEditor + ", isTeachingAssistant=" + isTeachingAssistant + ", isStudent=" + + isStudent + '}'; } } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/exception/NoUniqueQueryException.java b/src/main/java/de/tum/cit/aet/artemis/core/exception/NoUniqueQueryException.java new file mode 100644 index 000000000000..f76fc32879ed --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/core/exception/NoUniqueQueryException.java @@ -0,0 +1,11 @@ +package de.tum.cit.aet.artemis.core.exception; + +/** + * Checked exception in case a query does not return a unique result, so calling methods must handle this case. + */ +public class NoUniqueQueryException extends Exception { + + public NoUniqueQueryException(String message) { + super(message); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/annotations/AnnotationUtils.java b/src/main/java/de/tum/cit/aet/artemis/core/security/annotations/AnnotationUtils.java index c5aef335defb..028342bfeee9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/annotations/AnnotationUtils.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/annotations/AnnotationUtils.java @@ -3,7 +3,9 @@ import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Optional; import jakarta.validation.constraints.NotBlank; @@ -22,7 +24,8 @@ private AnnotationUtils() { } /** - * Extracts the annotation from the method or type + * Extracts the annotation from the method or type and all super classes. + * In case multiple versions of the annotation are present, the one closest to the method is returned. * * @param clazz the annotation class * @param joinPoint the join point @@ -33,26 +36,71 @@ private AnnotationUtils() { public static Optional getAnnotation(@NotNull Class clazz, @NotNull ProceedingJoinPoint joinPoint) { final var method = ((MethodSignature) joinPoint.getSignature()).getMethod(); T annotation = method.getAnnotation(clazz); + + Optional foundAnnotation = getAnnotation(clazz, method.getDeclaredAnnotations(), annotation); + if (foundAnnotation.isPresent()) { + return foundAnnotation; + } + + for (Class declaringClass = method.getDeclaringClass(); declaringClass != null; declaringClass = declaringClass.getSuperclass()) { + annotation = declaringClass.getAnnotation(clazz); + foundAnnotation = getAnnotation(clazz, declaringClass.getDeclaredAnnotations(), annotation); + if (foundAnnotation.isPresent()) { + return foundAnnotation; + } + } + + return Optional.empty(); + } + + private static Optional getAnnotation(Class clazz, Annotation[] declaredAnnotations, T annotation) { if (annotation != null) { return Optional.of(annotation); } - for (Annotation a : method.getDeclaredAnnotations()) { + for (Annotation a : declaredAnnotations) { annotation = a.annotationType().getAnnotation(clazz); if (annotation != null) { return Optional.of(annotation); } } - annotation = method.getDeclaringClass().getAnnotation(clazz); + return Optional.empty(); + } + + /** + * Extracts the annotations from the method or type and all super classes. + * In case multiple versions of the annotation are present, all are returned. + * + * @param clazz the annotation class + * @param joinPoint the join point + * @param the type of the annotation + * @return the annotations if they are present, empty otherwise + */ + public static List getAnnotations(@NotNull Class clazz, @NotNull ProceedingJoinPoint joinPoint) { + List annotations = new ArrayList<>(); + + final var method = ((MethodSignature) joinPoint.getSignature()).getMethod(); + T annotation = method.getAnnotation(clazz); + + addAnnotations(clazz, method.getDeclaredAnnotations(), annotation, annotations); + + for (Class declaringClass = method.getDeclaringClass(); declaringClass != null; declaringClass = declaringClass.getSuperclass()) { + annotation = declaringClass.getAnnotation(clazz); + addAnnotations(clazz, declaringClass.getDeclaredAnnotations(), annotation, annotations); + } + + return annotations; + } + + private static void addAnnotations(Class clazz, Annotation[] declaredAnnotations, T annotation, List annotations) { if (annotation != null) { - return Optional.of(annotation); + annotations.add(annotation); } - for (Annotation a : method.getDeclaringClass().getDeclaredAnnotations()) { + for (Annotation a : declaredAnnotations) { annotation = a.annotationType().getAnnotation(clazz); if (annotation != null) { - return Optional.of(annotation); + annotations.add(annotation); } } - return Optional.empty(); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/annotations/EnforceAdmin.java b/src/main/java/de/tum/cit/aet/artemis/core/security/annotations/EnforceAdmin.java index 5adbcd73c16c..9fdbf88d9d82 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/annotations/EnforceAdmin.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/annotations/EnforceAdmin.java @@ -10,11 +10,8 @@ /** * This annotation is used to enforce that the user is an admin. * It should only be used with endpoints starting with {@code /api/admin/} - *

- * It's only addable to methods. The intention is that a developer can see the required role without the need to scroll up. - * This also prevents overrides of the annotation. */ -@Target(ElementType.METHOD) +@Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @PreAuthorize("hasRole('ADMIN')") public @interface EnforceAdmin { diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java index 9e3b69f269cc..cba16e58ad7e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java @@ -58,8 +58,12 @@ import de.tum.cit.aet.artemis.atlas.repository.PrerequisiteRepository; import de.tum.cit.aet.artemis.atlas.service.learningpath.LearningPathService; import de.tum.cit.aet.artemis.communication.domain.NotificationType; +import de.tum.cit.aet.artemis.communication.domain.Post; import de.tum.cit.aet.artemis.communication.domain.notification.GroupNotification; +import de.tum.cit.aet.artemis.communication.repository.AnswerPostRepository; +import de.tum.cit.aet.artemis.communication.repository.FaqRepository; import de.tum.cit.aet.artemis.communication.repository.GroupNotificationRepository; +import de.tum.cit.aet.artemis.communication.repository.PostRepository; import de.tum.cit.aet.artemis.communication.repository.conversation.ConversationRepository; import de.tum.cit.aet.artemis.communication.service.notifications.GroupNotificationService; import de.tum.cit.aet.artemis.core.config.Constants; @@ -67,6 +71,7 @@ import de.tum.cit.aet.artemis.core.domain.DomainObject; import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.core.dto.CourseContentCount; +import de.tum.cit.aet.artemis.core.dto.CourseDeletionSummaryDTO; import de.tum.cit.aet.artemis.core.dto.CourseManagementDetailViewDTO; import de.tum.cit.aet.artemis.core.dto.DueDateStat; import de.tum.cit.aet.artemis.core.dto.SearchResultPageDTO; @@ -103,6 +108,7 @@ import de.tum.cit.aet.artemis.plagiarism.domain.PlagiarismCase; import de.tum.cit.aet.artemis.plagiarism.repository.PlagiarismCaseRepository; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; +import de.tum.cit.aet.artemis.programming.repository.BuildJobRepository; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseRepository; import de.tum.cit.aet.artemis.tutorialgroup.repository.TutorialGroupNotificationRepository; import de.tum.cit.aet.artemis.tutorialgroup.repository.TutorialGroupRepository; @@ -117,6 +123,8 @@ public class CourseService { private static final Logger log = LoggerFactory.getLogger(CourseService.class); + private final FaqRepository faqRepository; + @Value("${artemis.course-archives-path}") private Path courseArchivesDirPath; @@ -198,6 +206,12 @@ public class CourseService { private final TutorialGroupNotificationRepository tutorialGroupNotificationRepository; + private final PostRepository postRepository; + + private final AnswerPostRepository answerPostRepository; + + private final BuildJobRepository buildJobRepository; + public CourseService(CourseRepository courseRepository, ExerciseService exerciseService, ExerciseDeletionService exerciseDeletionService, AuthorizationCheckService authCheckService, UserRepository userRepository, LectureService lectureService, GroupNotificationRepository groupNotificationRepository, ExerciseGroupRepository exerciseGroupRepository, AuditEventRepository auditEventRepository, UserService userService, ExamDeletionService examDeletionService, @@ -210,7 +224,8 @@ public CourseService(CourseRepository courseRepository, ExerciseService exercise TutorialGroupRepository tutorialGroupRepository, PlagiarismCaseRepository plagiarismCaseRepository, ConversationRepository conversationRepository, LearningPathService learningPathService, Optional irisSettingsService, LectureRepository lectureRepository, TutorialGroupNotificationRepository tutorialGroupNotificationRepository, TutorialGroupChannelManagementService tutorialGroupChannelManagementService, - PrerequisiteRepository prerequisiteRepository, CompetencyRelationRepository competencyRelationRepository) { + PrerequisiteRepository prerequisiteRepository, CompetencyRelationRepository competencyRelationRepository, PostRepository postRepository, + AnswerPostRepository answerPostRepository, BuildJobRepository buildJobRepository, FaqRepository faqRepository) { this.courseRepository = courseRepository; this.exerciseService = exerciseService; this.exerciseDeletionService = exerciseDeletionService; @@ -250,6 +265,10 @@ public CourseService(CourseRepository courseRepository, ExerciseService exercise this.tutorialGroupChannelManagementService = tutorialGroupChannelManagementService; this.prerequisiteRepository = prerequisiteRepository; this.competencyRelationRepository = competencyRelationRepository; + this.buildJobRepository = buildJobRepository; + this.postRepository = postRepository; + this.answerPostRepository = answerPostRepository; + this.faqRepository = faqRepository; } /** @@ -440,6 +459,22 @@ public Set findAllOnlineCoursesForPlatformForUser(String registrationId, .collect(Collectors.toSet()); } + /** + * Get the course deletion summary for the given course. + * + * @param course the course for which to get the deletion summary + * @return the course deletion summary + */ + public CourseDeletionSummaryDTO getDeletionSummary(Course course) { + List programmingExerciseIds = course.getExercises().stream().map(Exercise::getId).toList(); + long numberOfBuilds = buildJobRepository.countBuildJobsByExerciseIds(programmingExerciseIds); + + List posts = postRepository.findAllByCourseId(course.getId()); + long numberOfCommunicationPosts = posts.size(); + long numberOfAnswerPosts = answerPostRepository.countAnswerPostsByPostIdIn(posts.stream().map(Post::getId).toList()); + return new CourseDeletionSummaryDTO(numberOfBuilds, numberOfCommunicationPosts, numberOfAnswerPosts); + } + /** * Deletes all elements associated with the course including: *

    @@ -467,6 +502,7 @@ public void delete(Course course) { deleteDefaultGroups(course); deleteExamsOfCourse(course); deleteGradingScaleOfCourse(course); + deleteFaqsOfCourse(course); irisSettingsService.ifPresent(iss -> iss.deleteSettingsFor(course)); courseRepository.deleteById(course.getId()); log.debug("Successfully deleted course {}.", course.getTitle()); @@ -542,6 +578,10 @@ private void deleteCompetenciesOfCourse(Course course) { competencyRepository.deleteAll(course.getCompetencies()); } + private void deleteFaqsOfCourse(Course course) { + faqRepository.deleteAllByCourseId(course.getId()); + } + /** * If the exercise is part of an exam, retrieve the course through ExerciseGroup -> Exam -> Course. * Otherwise, the course is already set and the id can be used to retrieve the course from the database. diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/ZipFileService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/ZipFileService.java index 4d40473c4eb9..5871cd7ed7d4 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/ZipFileService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/ZipFileService.java @@ -6,6 +6,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import java.util.Set; import java.util.function.Predicate; import java.util.stream.Stream; import java.util.zip.ZipEntry; @@ -33,6 +34,12 @@ public class ZipFileService { private final FileService fileService; + /** + * Set of file names that should be ignored when zipping. + * This currently only includes the gc.log.lock (garbage collector) file created by JGit in programming repositories. + */ + private static final Set IGNORED_ZIP_FILE_NAMES = Set.of(Path.of("gc.log.lock")); + public ZipFileService(FileService fileService) { this.fileService = fileService; } @@ -113,7 +120,7 @@ private void createZipFileFromPathStream(Path zipFilePath, Stream paths, P if (extraFilter != null) { filteredPaths = filteredPaths.filter(extraFilter); } - filteredPaths.forEach(path -> { + filteredPaths.filter(path -> !IGNORED_ZIP_FILE_NAMES.contains(path)).forEach(path -> { ZipEntry zipEntry = new ZipEntry(pathsRoot.relativize(path).toString()); copyToZipFile(zipOutputStream, path, zipEntry); }); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/feature/FeatureToggleAspect.java b/src/main/java/de/tum/cit/aet/artemis/core/service/feature/FeatureToggleAspect.java index 87785e123cec..ffeed9cfa513 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/feature/FeatureToggleAspect.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/feature/FeatureToggleAspect.java @@ -1,8 +1,11 @@ package de.tum.cit.aet.artemis.core.service.feature; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; +import static de.tum.cit.aet.artemis.core.security.annotations.AnnotationUtils.getAnnotations; import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; @@ -26,25 +29,24 @@ public FeatureToggleAspect(FeatureToggleService featureToggleService) { /** * Pointcut around all methods or classes annotated with {@link FeatureToggle}. - * - * @param featureToggle The feature toggle annotation containing the relevant features */ - @Pointcut("@within(featureToggle) || @annotation(featureToggle)") - public void callAt(FeatureToggle featureToggle) { + @Pointcut("@within(de.tum.cit.aet.artemis.core.service.feature.FeatureToggle) || @annotation(de.tum.cit.aet.artemis.core.service.feature.FeatureToggle) || execution(@(@de.tum.cit.aet.artemis.core.service.feature.FeatureToggle *) * *(..))") + protected void callAt() { } /** * Aspect around all methods for which a feature toggle has been activated. Will check all specified features and only * execute the underlying method if all features are enabled. Will otherwise return forbidden (as response entity) * - * @param joinPoint Proceeding join point of the aspect - * @param featureToggle The feature toggle annotation containing all features that should get checked + * @param joinPoint Proceeding join point of the aspect * @return The original return value of the called method, if all features are enabled, a forbidden response entity otherwise * @throws Throwable If there was any error during method execution (both the aspect or the actual called method) */ - @Around(value = "callAt(featureToggle)", argNames = "joinPoint,featureToggle") - public Object around(ProceedingJoinPoint joinPoint, FeatureToggle featureToggle) throws Throwable { - if (Arrays.stream(featureToggle.value()).allMatch(featureToggleService::isFeatureEnabled)) { + @Around(value = "callAt()", argNames = "joinPoint") + public Object around(ProceedingJoinPoint joinPoint) throws Throwable { + List featureToggleAnnotations = getAnnotations(FeatureToggle.class, joinPoint); + Stream features = featureToggleAnnotations.stream().flatMap(featureToggle -> Arrays.stream(featureToggle.value())); + if (features.allMatch(featureToggleService::isFeatureEnabled)) { return joinPoint.proceed(); } else { diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java index 10da69a96a5d..0cb3379e4f99 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java @@ -364,14 +364,14 @@ else if (courseUpdate.getCourseIcon() == null && existingCourse.getCourseIcon() } /** - * PUT courses/:courseId/onlineCourseConfiguration : Updates the onlineCourseConfiguration for the given course. + * PUT courses/:courseId/online-course-configuration : Updates the onlineCourseConfiguration for the given course. * * @param courseId the id of the course to update * @param onlineCourseConfiguration the online course configuration to update * @return the ResponseEntity with status 200 (OK) and with body the updated online course configuration */ // TODO: move into LTIResource - @PutMapping("courses/{courseId}/onlineCourseConfiguration") + @PutMapping("courses/{courseId}/online-course-configuration") @EnforceAtLeastInstructor @Profile(PROFILE_LTI) public ResponseEntity updateOnlineCourseConfiguration(@PathVariable Long courseId, @@ -821,12 +821,12 @@ public ResponseEntity getCourseWithOrganizations(@PathVariable Long cour } /** - * GET /courses/:courseId/lockedSubmissions Get locked submissions for course for user + * GET /courses/:courseId/locked-submissions Get locked submissions for course for user * * @param courseId the id of the course * @return the ResponseEntity with status 200 (OK) and with body the course, or with status 404 (Not Found) */ - @GetMapping("courses/{courseId}/lockedSubmissions") + @GetMapping("courses/{courseId}/locked-submissions") @EnforceAtLeastTutor public ResponseEntity> getLockedSubmissionsForCourse(@PathVariable Long courseId) { log.debug("REST request to get all locked submissions for course : {}", courseId); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AuditResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminAuditResource.java similarity index 96% rename from src/main/java/de/tum/cit/aet/artemis/core/web/admin/AuditResource.java rename to src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminAuditResource.java index fe8af9ff7eb0..7c0a339d9717 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AuditResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminAuditResource.java @@ -30,13 +30,14 @@ * REST controller for getting the audit events. */ @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") -public class AuditResource { +public class AdminAuditResource { private final AuditEventService auditEventService; - public AuditResource(AuditEventService auditEventService) { + public AdminAuditResource(AuditEventService auditEventService) { this.auditEventService = auditEventService; } @@ -47,7 +48,6 @@ public AuditResource(AuditEventService auditEventService) { * @return the ResponseEntity with status 200 (OK) and the list of AuditEvents in body */ @GetMapping("audits") - @EnforceAdmin public ResponseEntity> getAll(Pageable pageable) { Page page = auditEventService.findAll(pageable); HttpHeaders headers = generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); @@ -63,7 +63,6 @@ public ResponseEntity> getAll(Pageable pageable) { * @return the ResponseEntity with status 200 (OK) and the list of AuditEvents in body */ @GetMapping(value = "audits", params = { "fromDate", "toDate" }) - @EnforceAdmin public ResponseEntity> getByDates(@RequestParam(value = "fromDate") LocalDate fromDate, @RequestParam(value = "toDate") LocalDate toDate, Pageable pageable) { Instant from = fromDate.atStartOfDay(ZoneId.systemDefault()).toInstant(); @@ -81,7 +80,6 @@ public ResponseEntity> getByDates(@RequestParam(value = "fromDa * @return the ResponseEntity with status 200 (OK) and the AuditEvent in body, or status 404 (Not Found) */ @GetMapping("audits/{id:.+}") - @EnforceAdmin public ResponseEntity get(@PathVariable Long id) { return ResponseUtil.wrapOrNotFound(auditEventService.find(id)); } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminBuildJobQueueResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminBuildJobQueueResource.java index bd1bcc4dea1b..db71ab34c05a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminBuildJobQueueResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminBuildJobQueueResource.java @@ -33,6 +33,7 @@ import tech.jhipster.web.util.PaginationUtil; @Profile(PROFILE_LOCALCI) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminBuildJobQueueResource { @@ -54,7 +55,6 @@ public AdminBuildJobQueueResource(SharedQueueManagementService localCIBuildJobQu * @return the queued build jobs */ @GetMapping("queued-jobs") - @EnforceAdmin public ResponseEntity> getQueuedBuildJobs() { log.debug("REST request to get the queued build jobs"); List buildJobQueue = localCIBuildJobQueueService.getQueuedJobs(); @@ -67,7 +67,6 @@ public ResponseEntity> getQueuedBuildJobs() { * @return the running build jobs */ @GetMapping("running-jobs") - @EnforceAdmin public ResponseEntity> getRunningBuildJobs() { log.debug("REST request to get the running build jobs"); List runningBuildJobs = localCIBuildJobQueueService.getProcessingJobs(); @@ -80,7 +79,6 @@ public ResponseEntity> getRunningBuildJobs() { * @return list of build agents information */ @GetMapping("build-agents") - @EnforceAdmin public ResponseEntity> getBuildAgentSummary() { log.debug("REST request to get information on available build agents"); List buildAgentSummary = localCIBuildJobQueueService.getBuildAgentInformationWithoutRecentBuildJobs(); @@ -94,7 +92,6 @@ public ResponseEntity> getBuildAgentSummary() { * @return the build agent information */ @GetMapping("build-agent") - @EnforceAdmin public ResponseEntity getBuildAgentDetails(@RequestParam String agentName) { log.debug("REST request to get information on build agent {}", agentName); BuildAgentInformation buildAgentDetails = localCIBuildJobQueueService.getBuildAgentInformation().stream().filter(agent -> agent.name().equals(agentName)).findFirst() @@ -109,7 +106,6 @@ public ResponseEntity getBuildAgentDetails(@RequestParam * @return the ResponseEntity with the result of the cancellation */ @DeleteMapping("cancel-job/{buildJobId}") - @EnforceAdmin public ResponseEntity cancelBuildJob(@PathVariable String buildJobId) { log.debug("REST request to cancel the build job with id {}", buildJobId); // Call the cancelBuildJob method in LocalCIBuildJobManagementService @@ -124,7 +120,6 @@ public ResponseEntity cancelBuildJob(@PathVariable String buildJobId) { * @return the ResponseEntity with the result of the cancellation */ @DeleteMapping("cancel-all-queued-jobs") - @EnforceAdmin public ResponseEntity cancelAllQueuedBuildJobs() { log.debug("REST request to cancel all queued build jobs"); // Call the cancelAllQueuedBuildJobs method in LocalCIBuildJobManagementService @@ -139,7 +134,6 @@ public ResponseEntity cancelAllQueuedBuildJobs() { * @return the ResponseEntity with the result of the cancellation */ @DeleteMapping("cancel-all-running-jobs") - @EnforceAdmin public ResponseEntity cancelAllRunningBuildJobs() { log.debug("REST request to cancel all running build jobs"); // Call the cancelAllRunningBuildJobs method in LocalCIBuildJobManagementService @@ -155,7 +149,6 @@ public ResponseEntity cancelAllRunningBuildJobs() { * @return the ResponseEntity with the result of the cancellation */ @DeleteMapping("cancel-all-running-jobs-for-agent") - @EnforceAdmin public ResponseEntity cancelAllRunningBuildJobsForAgent(@RequestParam String agentName) { log.debug("REST request to cancel all running build jobs for agent {}", agentName); // Call the cancelAllRunningBuildJobsForAgent method in LocalCIBuildJobManagementService @@ -171,7 +164,6 @@ public ResponseEntity cancelAllRunningBuildJobsForAgent(@RequestParam Stri * @return the page of finished build jobs */ @GetMapping("finished-jobs") - @EnforceAdmin public ResponseEntity> getFinishedBuildJobs(FinishedBuildJobPageableSearchDTO search) { log.debug("REST request to get a page of finished build jobs with build status {}, build agent address {}, start date {} and end date {}", search.buildStatus(), search.buildAgentAddress(), search.startDate(), search.endDate()); @@ -190,7 +182,6 @@ public ResponseEntity> getFinishedBuildJobs(FinishedBu * @return the build job statistics */ @GetMapping("build-job-statistics") - @EnforceAdmin public ResponseEntity getBuildJobStatistics(@RequestParam(required = false, defaultValue = "7") int span) { log.debug("REST request to get the build job statistics"); List buildJobResultCountDtos = buildJobRepository.getBuildJobsResultsStatistics(ZonedDateTime.now().minusDays(span), null); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminCourseResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminCourseResource.java index a75b2e2d4a0a..6c26cab4798f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminCourseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminCourseResource.java @@ -34,6 +34,7 @@ import de.tum.cit.aet.artemis.core.config.Constants; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.core.dto.CourseDeletionSummaryDTO; import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; import de.tum.cit.aet.artemis.core.repository.CourseRepository; import de.tum.cit.aet.artemis.core.repository.UserRepository; @@ -48,6 +49,7 @@ * REST controller for managing Course. */ @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminCourseResource { @@ -90,7 +92,6 @@ public AdminCourseResource(UserRepository userRepository, CourseService courseSe * @return the list of groups (the user has access to) */ @GetMapping("courses/groups") - @EnforceAdmin public ResponseEntity> getAllGroupsForAllCourses() { log.debug("REST request to get all Groups for all Courses"); List courses = courseRepository.findAll(); @@ -113,7 +114,6 @@ public ResponseEntity> getAllGroupsForAllCourses() { * @throws URISyntaxException if the Location URI syntax is incorrect */ @PostMapping(value = "courses", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - @EnforceAdmin public ResponseEntity createCourse(@RequestPart Course course, @RequestPart(required = false) MultipartFile file) throws URISyntaxException { log.debug("REST request to save Course : {}", course); if (course.getId() != null) { @@ -167,7 +167,6 @@ public ResponseEntity createCourse(@RequestPart Course course, @RequestP * @return the ResponseEntity with status 200 (OK) */ @DeleteMapping("courses/{courseId}") - @EnforceAdmin public ResponseEntity deleteCourse(@PathVariable long courseId) { log.info("REST request to delete Course : {}", courseId); Course course = courseRepository.findByIdWithExercisesAndLecturesAndLectureUnitsAndCompetenciesElseThrow(courseId); @@ -183,6 +182,20 @@ public ResponseEntity deleteCourse(@PathVariable long courseId) { return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, Course.ENTITY_NAME, course.getTitle())).build(); } + /** + * GET /courses/:courseId/deletion-summary : get the deletion summary for the course with the given id. + * + * @param courseId the id of the course + * @return the ResponseEntity with status 200 (OK) and the deletion summary in the body + */ + @GetMapping("courses/{courseId}/deletion-summary") + public ResponseEntity getDeletionSummary(@PathVariable long courseId) { + log.debug("REST request to get deletion summary course: {}", courseId); + final Course course = courseRepository.findByIdWithEagerExercisesElseThrow(courseId); + + return ResponseEntity.ok().body(courseService.getDeletionSummary(course)); + } + /** * Creates a default channel with the given name and adds all students, tutors and instructors as participants. * diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminDataExportResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminDataExportResource.java index 0e1f17de42c1..6547fd7ef57a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminDataExportResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminDataExportResource.java @@ -17,6 +17,7 @@ * REST controller for requesting data exports for another user as admin. */ @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminDataExportResource { @@ -34,7 +35,6 @@ public AdminDataExportResource(DataExportService dataExportService) { * @return the ResponseEntity with status 200 (OK) and with body a DTO containing the id, the state and the request date of the data export */ @PostMapping("data-exports/{login}") - @EnforceAdmin public ResponseEntity requestDataExportForUser(@PathVariable String login) { return ResponseEntity.ok(dataExportService.requestDataExportForUserAsAdmin(login)); } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/FeatureToggleResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminFeatureToggleResource.java similarity index 91% rename from src/main/java/de/tum/cit/aet/artemis/core/web/admin/FeatureToggleResource.java rename to src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminFeatureToggleResource.java index 84a58f7918aa..e2d24720efbd 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/FeatureToggleResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminFeatureToggleResource.java @@ -18,13 +18,14 @@ import de.tum.cit.aet.artemis.core.service.feature.FeatureToggleService; @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") -public class FeatureToggleResource { +public class AdminFeatureToggleResource { private final FeatureToggleService featureToggleService; - public FeatureToggleResource(FeatureToggleService featureToggleService) { + public AdminFeatureToggleResource(FeatureToggleService featureToggleService) { this.featureToggleService = featureToggleService; } @@ -36,7 +37,6 @@ public FeatureToggleResource(FeatureToggleService featureToggleService) { * @see FeatureToggleService */ @PutMapping("feature-toggle") - @EnforceAdmin public ResponseEntity> toggleFeatures(@RequestBody Map features) { featureToggleService.updateFeatureToggles(features); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminImprintResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminImprintResource.java index 1e705926232c..58c99393679a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminImprintResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminImprintResource.java @@ -22,6 +22,7 @@ * REST controller for editing the imprint as an admin. */ @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminImprintResource { @@ -40,7 +41,6 @@ public AdminImprintResource(LegalDocumentService legalDocumentService) { * @return the ResponseEntity with status 200 (OK) and with body the imprint with the given language */ @GetMapping("imprint-for-update") - @EnforceAdmin public ResponseEntity getImprintForUpdate(@RequestParam("language") String language) { if (!Language.isValidShortName(language)) { throw new BadRequestException("Language not supported"); @@ -55,7 +55,6 @@ public ResponseEntity getImprintForUpdate(@RequestParam("language") * @return the ResponseEntity with status 200 (OK) and with body the updated imprint */ @PutMapping("imprint") - @EnforceAdmin public ResponseEntity updateImprint(@RequestBody ImprintDTO imprint) { return ResponseEntity.ok(legalDocumentService.updateImprint(imprint)); } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/LogResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminLogResource.java similarity index 96% rename from src/main/java/de/tum/cit/aet/artemis/core/web/admin/LogResource.java rename to src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminLogResource.java index 375deea352f6..a0855aa92929 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/LogResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminLogResource.java @@ -23,9 +23,10 @@ * Controller for view and managing Log Level at runtime. */ @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") -public class LogResource { +public class AdminLogResource { /** * GET logs -- Gets the current log levels. @@ -33,7 +34,6 @@ public class LogResource { * @return A list of all loggers with their log level */ @GetMapping("logs") - @EnforceAdmin public ResponseEntity> getList() { LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); return ResponseEntity.ok(context.getLoggerList().stream().map(LoggerVM::new).toList()); @@ -46,7 +46,6 @@ public ResponseEntity> getList() { * @return The updated logger */ @PutMapping("logs") - @EnforceAdmin public ResponseEntity changeLevel(@RequestBody LoggerVM jsonLogger) { LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); Logger logger = context.getLogger(jsonLogger.getName()); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminOrganizationResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminOrganizationResource.java index 75d3719b9c6e..c62673b6178f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminOrganizationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminOrganizationResource.java @@ -36,6 +36,7 @@ * REST controller for administrating the Organization entities */ @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminOrganizationResource { @@ -72,7 +73,6 @@ public AdminOrganizationResource(OrganizationService organizationService, Organi * @return empty ResponseEntity with status 200 (OK), or 404 (Not Found) otherwise */ @PostMapping("organizations/{organizationId}/courses/{courseId}") - @EnforceAdmin public ResponseEntity addCourseToOrganization(@PathVariable Long courseId, @PathVariable Long organizationId) { log.debug("REST request to add course to organization : {}", organizationId); Organization organization = organizationRepository.findByIdElseThrow(organizationId); @@ -90,7 +90,6 @@ public ResponseEntity addCourseToOrganization(@PathVariable Long courseId, * @return empty ResponseEntity with status 200 (OK), or 404 (Not Found) otherwise */ @DeleteMapping("organizations/{organizationId}/courses/{courseId}") - @EnforceAdmin public ResponseEntity removeCourseFromOrganization(@PathVariable Long courseId, @PathVariable Long organizationId) { Organization organization = organizationRepository.findByIdElseThrow(organizationId); courseRepository.removeOrganizationFromCourse(courseId, organization); @@ -107,7 +106,6 @@ public ResponseEntity removeCourseFromOrganization(@PathVariable Long cour * @return empty ResponseEntity with status 200 (OK), or 404 (Not Found) otherwise */ @PostMapping("organizations/{organizationId}/users/{userLogin}") - @EnforceAdmin public ResponseEntity addUserToOrganization(@PathVariable String userLogin, @PathVariable Long organizationId) { User user = userRepository.getUserByLoginElseThrow(userLogin); Organization organization = organizationRepository.findByIdElseThrow(organizationId); @@ -128,7 +126,6 @@ public ResponseEntity addUserToOrganization(@PathVariable String userLogin * @return empty ResponseEntity with status 200 (OK), or 404 (Not Found) otherwise */ @DeleteMapping("organizations/{organizationId}/users/{userLogin}") - @EnforceAdmin public ResponseEntity removeUserFromOrganization(@PathVariable String userLogin, @PathVariable Long organizationId) { log.debug("REST request to remove course to organization : {}", organizationId); User user = userRepository.getUserByLoginElseThrow(userLogin); @@ -145,7 +142,6 @@ public ResponseEntity removeUserFromOrganization(@PathVariable String user * @return the ResponseEntity containing the added organization with status 200 (OK), or 404 (Not Found) otherwise */ @PostMapping("organizations") - @EnforceAdmin public ResponseEntity addOrganization(@RequestBody Organization organization) { log.debug("REST request to add new organization : {}", organization); Organization created = organizationService.add(organization); @@ -161,7 +157,6 @@ public ResponseEntity addOrganization(@RequestBody Organization or * @return the ResponseEntity containing the updated organization with status 200 (OK), or 404 (Not Found) otherwise */ @PutMapping("organizations/{organizationId}") - @EnforceAdmin public ResponseEntity updateOrganization(@PathVariable Long organizationId, @RequestBody Organization organization) { log.debug("REST request to update organization : {}", organization); if (organization.getId() == null) { @@ -182,7 +177,6 @@ public ResponseEntity updateOrganization(@PathVariable Long organi * @return empty ResponseEntity with status 200 (OK), or 404 (Not Found) otherwise */ @DeleteMapping("organizations/{organizationId}") - @EnforceAdmin public ResponseEntity deleteOrganization(@PathVariable Long organizationId) { log.debug("REST request to delete organization : {}", organizationId); organizationService.deleteOrganization(organizationId); @@ -195,7 +189,6 @@ public ResponseEntity deleteOrganization(@PathVariable Long organizationId * @return ResponseEntity containing a list of all organizations with status 200 (OK) */ @GetMapping("organizations") - @EnforceAdmin public ResponseEntity> getAllOrganizations() { log.debug("REST request to get all organizations"); // TODO: we should avoid findAll() and instead load batches of organizations @@ -210,7 +203,6 @@ public ResponseEntity> getAllOrganizations() { * @return ResponseEntity containing a map containing the numbers of users and courses */ @GetMapping("organizations/{organizationId}/count") - @EnforceAdmin public ResponseEntity getNumberOfUsersAndCoursesByOrganization(@PathVariable long organizationId) { log.debug("REST request to get number of users and courses of organization : {}", organizationId); @@ -227,7 +219,6 @@ public ResponseEntity getNumberOfUsersAndCoursesByOrganiza * containing their relative numbers of users and courses */ @GetMapping("organizations/count-all") - @EnforceAdmin public ResponseEntity> getNumberOfUsersAndCoursesOfAllOrganizations() { log.debug("REST request to get number of users and courses of all organizations"); @@ -250,7 +241,6 @@ public ResponseEntity> getNumberOfUsersAndCoursesOfAl * if exists, else with status 404 (Not Found) */ @GetMapping("organizations/{organizationId}") - @EnforceAdmin public ResponseEntity getOrganizationById(@PathVariable long organizationId) { log.debug("REST request to get organization : {}", organizationId); Organization organization = organizationRepository.findByIdElseThrow(organizationId); @@ -265,7 +255,6 @@ public ResponseEntity getOrganizationById(@PathVariable long organ * if exists, else with status 404 (Not Found) */ @GetMapping("organizations/{organizationId}/full") - @EnforceAdmin public ResponseEntity getOrganizationByIdWithUsersAndCourses(@PathVariable long organizationId) { log.debug("REST request to get organization with users and courses : {}", organizationId); Organization organization = organizationRepository.findByIdWithEagerUsersAndCoursesElseThrow(organizationId); @@ -279,7 +268,6 @@ public ResponseEntity getOrganizationByIdWithUsersAndCourses(@Path * @return ResponseEntity containing a set of organizations containing the given user */ @GetMapping("organizations/users/{userId}") - @EnforceAdmin public ResponseEntity> getAllOrganizationsByUser(@PathVariable Long userId) { log.debug("REST request to get all organizations of user : {}", userId); Set organizations = organizationRepository.findAllOrganizationsByUserId(userId); @@ -293,7 +281,6 @@ public ResponseEntity> getAllOrganizationsByUser(@PathVariable * @return the title of the organization wrapped in an ResponseEntity or 404 Not Found if no organization with that id exists */ @GetMapping("organizations/{organizationId}/title") - @EnforceAdmin public ResponseEntity getOrganizationTitle(@PathVariable Long organizationId) { final var title = organizationRepository.getOrganizationTitle(organizationId); return title == null ? ResponseEntity.notFound().build() : ResponseEntity.ok(title); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminPrivacyStatementResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminPrivacyStatementResource.java index af48dc8f3565..736d926218ab 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminPrivacyStatementResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminPrivacyStatementResource.java @@ -22,6 +22,7 @@ * REST controller for editing the Privacy Statement as an admin. */ @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminPrivacyStatementResource { @@ -39,7 +40,6 @@ public AdminPrivacyStatementResource(LegalDocumentService legalDocumentService) * @param language the language of the privacy statement * @return the ResponseEntity with status 200 (OK) and with body the privacy statement */ - @EnforceAdmin @GetMapping("privacy-statement-for-update") public ResponseEntity getPrivacyStatementForUpdate(@RequestParam("language") String language) { if (!Language.isValidShortName(language)) { @@ -54,7 +54,6 @@ public ResponseEntity getPrivacyStatementForUpdate(@Request * @param privacyStatement the privacy statement to update * @return the ResponseEntity with status 200 (OK) and with body the updated privacy statement */ - @EnforceAdmin @PutMapping("privacy-statement") public ResponseEntity updatePrivacyStatement(@RequestBody PrivacyStatementDTO privacyStatement) { return ResponseEntity.ok(legalDocumentService.updatePrivacyStatement(privacyStatement)); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminStatisticsResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminStatisticsResource.java index 9c6db4cc7616..c91d7df176c3 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminStatisticsResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminStatisticsResource.java @@ -23,6 +23,7 @@ * REST controller for administrating statistics. */ @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminStatisticsResource { @@ -44,7 +45,6 @@ public AdminStatisticsResource(StatisticsService statisticsService) { * @return the ResponseEntity with status 200 (OK) and the data in body, or status 404 (Not Found) */ @GetMapping("management/statistics/data") - @EnforceAdmin public ResponseEntity> getChartData(@RequestParam SpanType span, @RequestParam Integer periodIndex, @RequestParam GraphType graphType) { log.debug("REST request to get graph data"); return ResponseEntity.ok(this.statisticsService.getChartData(span, periodIndex, graphType, StatisticsView.ARTEMIS, null)); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminUserResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminUserResource.java index 289d296c5624..3f25c36d8a67 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminUserResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminUserResource.java @@ -25,7 +25,6 @@ import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; @@ -69,6 +68,7 @@ * Another option would be to have a specific JPA entity graph to handle this case. */ @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminUserResource { @@ -108,7 +108,6 @@ public AdminUserResource(UserRepository userRepository, UserService userService, * @throws BadRequestAlertException 400 (Bad Request) if the login or email is already in use */ @PostMapping("users") - @EnforceAdmin public ResponseEntity createUser(@Valid @RequestBody ManagedUserVM managedUserVM) throws URISyntaxException { this.userService.checkUsernameAndPasswordValidityElseThrow(managedUserVM.getLogin(), managedUserVM.getPassword()); @@ -144,7 +143,6 @@ else if (userRepository.findOneByEmailIgnoreCase(managedUserVM.getEmail()).isPre * @throws LoginAlreadyUsedException 400 (Bad Request) if the login is already in use */ @PutMapping("users") - @EnforceAdmin public ResponseEntity updateUser(@Valid @RequestBody ManagedUserVM managedUserVM) { this.userService.checkUsernameAndPasswordValidityElseThrow(managedUserVM.getLogin(), managedUserVM.getPassword()); log.debug("REST request to update User : {}", managedUserVM); @@ -181,7 +179,6 @@ public ResponseEntity updateUser(@Valid @RequestBody ManagedUserVM mana * @return the ResponseEntity with status 200 (OK) and with body the "login" user, or with status 404 (Not Found) */ @GetMapping("users/{login:" + Constants.LOGIN_REGEX + "}") - @EnforceAdmin public ResponseEntity getUser(@PathVariable String login) { log.debug("REST request to get User : {}", login); return ResponseUtil.wrapOrNotFound(userRepository.findOneWithGroupsAndAuthoritiesByLogin(login).map(user -> { @@ -201,7 +198,6 @@ public ResponseEntity getUser(@PathVariable String login) { * @return the list of users who could not be imported, because they could NOT be found in the Artemis database and could NOT be found in the connected LDAP */ @PostMapping("users/import") - @EnforceAdmin public ResponseEntity> importUsers(@RequestBody List userDtos) { log.debug("REST request to import {} to Artemis", userDtos); List notFoundStudentsDtos = userService.importUsers(userDtos); @@ -215,7 +211,6 @@ public ResponseEntity> importUsers(@RequestBody List syncUserViaLdap(@PathVariable Long userId) { log.debug("REST request to update ldap information User : {}", userId); @@ -235,7 +230,6 @@ public ResponseEntity syncUserViaLdap(@PathVariable Long userId) { * @return the ResponseEntity with status 200 (OK) and with body all users */ @GetMapping("users") - @EnforceAdmin public ResponseEntity> getAllUsers(UserPageableSearchDTO userSearch) { final Page page = userRepository.getAllManagedUsers(userSearch); HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); @@ -248,7 +242,6 @@ public ResponseEntity> getAllUsers(UserPageableSearchDTO userSearc * @return the ResponseEntity with status 200 (OK) and with body all logins of not enrolled users */ @GetMapping("users/not-enrolled") - @EnforceAdmin public ResponseEntity> getNotEnrolledUsers() { List logins = userRepository.findAllNotEnrolledUsers(); return new ResponseEntity<>(logins, HttpStatus.OK); @@ -260,7 +253,6 @@ public ResponseEntity> getNotEnrolledUsers() { * @return the ResponseEntity with status 200 (OK) and with body a string list of the all the roles */ @GetMapping("users/authorities") - @EnforceAdmin public ResponseEntity> getAuthorities() { return ResponseEntity.ok(authorityRepository.getAuthorities()); } @@ -272,7 +264,6 @@ public ResponseEntity> getAuthorities() { * @return the ResponseEntity with status 200 (OK) */ @DeleteMapping("users/{login:" + Constants.LOGIN_REGEX + "}") - @EnforceAdmin public ResponseEntity deleteUser(@PathVariable String login) { log.debug("REST request to delete User: {}", login); if (userRepository.isCurrentUser(login)) { @@ -289,8 +280,7 @@ public ResponseEntity deleteUser(@PathVariable String login) { * @return the ResponseEntity with status 200 (OK) */ @DeleteMapping("users") - @EnforceAdmin - public ResponseEntity> deleteUsers(@RequestParam(name = "login") List logins) { + public ResponseEntity> deleteUsers(@RequestBody List logins) { log.debug("REST request to delete {} users", logins.size()); List deletedUsers = Collections.synchronizedList(new java.util.ArrayList<>()); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicAccountResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicAccountResource.java index 992b340359a1..bd673ece51d4 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicAccountResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicAccountResource.java @@ -167,6 +167,7 @@ public ResponseEntity getAccount() { userDTO.setVcsAccessToken(user.getVcsAccessToken()); userDTO.setVcsAccessTokenExpiryDate(user.getVcsAccessTokenExpiryDate()); userDTO.setSshPublicKey(user.getSshPublicKey()); + userDTO.setSshKeyHash(user.getSshPublicKeyHash()); log.info("GET /account {} took {}ms", user.getLogin(), System.currentTimeMillis() - start); return ResponseEntity.ok(userDTO); } diff --git a/src/main/java/de/tum/cit/aet/artemis/exam/dto/ExamDeletionSummaryDTO.java b/src/main/java/de/tum/cit/aet/artemis/exam/dto/ExamDeletionSummaryDTO.java new file mode 100644 index 000000000000..c0cd6608ec2c --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/exam/dto/ExamDeletionSummaryDTO.java @@ -0,0 +1,8 @@ +package de.tum.cit.aet.artemis.exam.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record ExamDeletionSummaryDTO(long numberOfBuilds, long numberOfCommunicationPosts, long numberOfAnswerPosts, long numberRegisteredStudents, long numberNotStartedExams, + long numberStartedExams, long numberSubmittedExams) { +} diff --git a/src/main/java/de/tum/cit/aet/artemis/exam/repository/StudentExamRepository.java b/src/main/java/de/tum/cit/aet/artemis/exam/repository/StudentExamRepository.java index 2236d4d2035c..4d3bcfe61369 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exam/repository/StudentExamRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/exam/repository/StudentExamRepository.java @@ -245,6 +245,10 @@ SELECT MAX(se.workingTime) """) Set findAllUnsubmittedWithExercisesByExamId(@Param("examId") Long examId); + List findAllByExamId(Long examId); + + List findAllByExamId_AndTestRunIsTrue(Long examId); + @Query(""" SELECT DISTINCT se FROM StudentExam se diff --git a/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamDeletionService.java b/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamDeletionService.java index a8fd46512528..1c05e1d4be65 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamDeletionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamDeletionService.java @@ -20,7 +20,10 @@ import de.tum.cit.aet.artemis.assessment.domain.GradingScale; import de.tum.cit.aet.artemis.assessment.repository.GradingScaleRepository; +import de.tum.cit.aet.artemis.communication.domain.Post; import de.tum.cit.aet.artemis.communication.domain.conversation.Channel; +import de.tum.cit.aet.artemis.communication.repository.AnswerPostRepository; +import de.tum.cit.aet.artemis.communication.repository.PostRepository; import de.tum.cit.aet.artemis.communication.repository.conversation.ChannelRepository; import de.tum.cit.aet.artemis.communication.service.conversation.ChannelService; import de.tum.cit.aet.artemis.core.config.Constants; @@ -29,13 +32,16 @@ import de.tum.cit.aet.artemis.exam.domain.Exam; import de.tum.cit.aet.artemis.exam.domain.ExerciseGroup; import de.tum.cit.aet.artemis.exam.domain.StudentExam; +import de.tum.cit.aet.artemis.exam.dto.ExamDeletionSummaryDTO; import de.tum.cit.aet.artemis.exam.repository.ExamLiveEventRepository; import de.tum.cit.aet.artemis.exam.repository.ExamRepository; import de.tum.cit.aet.artemis.exam.repository.StudentExamRepository; import de.tum.cit.aet.artemis.exercise.domain.Exercise; +import de.tum.cit.aet.artemis.exercise.domain.ExerciseType; import de.tum.cit.aet.artemis.exercise.repository.StudentParticipationRepository; import de.tum.cit.aet.artemis.exercise.service.ExerciseDeletionService; import de.tum.cit.aet.artemis.exercise.service.ParticipationService; +import de.tum.cit.aet.artemis.programming.repository.BuildJobRepository; import de.tum.cit.aet.artemis.quiz.domain.QuizPool; import de.tum.cit.aet.artemis.quiz.repository.QuizPoolRepository; @@ -71,10 +77,17 @@ public class ExamDeletionService { private final QuizPoolRepository quizPoolRepository; + private final BuildJobRepository buildJobRepository; + + private final PostRepository postRepository; + + private final AnswerPostRepository answerPostRepository; + public ExamDeletionService(ExerciseDeletionService exerciseDeletionService, ParticipationService participationService, CacheManager cacheManager, UserRepository userRepository, ExamRepository examRepository, AuditEventRepository auditEventRepository, StudentExamRepository studentExamRepository, GradingScaleRepository gradingScaleRepository, StudentParticipationRepository studentParticipationRepository, ChannelRepository channelRepository, ChannelService channelService, - ExamLiveEventRepository examLiveEventRepository, QuizPoolRepository quizPoolRepository) { + ExamLiveEventRepository examLiveEventRepository, QuizPoolRepository quizPoolRepository, BuildJobRepository buildJobRepository, PostRepository postRepository, + AnswerPostRepository answerPostRepository) { this.exerciseDeletionService = exerciseDeletionService; this.participationService = participationService; this.cacheManager = cacheManager; @@ -88,6 +101,9 @@ public ExamDeletionService(ExerciseDeletionService exerciseDeletionService, Part this.channelService = channelService; this.examLiveEventRepository = examLiveEventRepository; this.quizPoolRepository = quizPoolRepository; + this.buildJobRepository = buildJobRepository; + this.postRepository = postRepository; + this.answerPostRepository = answerPostRepository; } /** @@ -240,4 +256,34 @@ public void deleteTestRun(Long testRunId) { log.info("Request to delete Test Run {}", testRunId); studentExamRepository.deleteById(testRunId); } + + /** + * Get the exam deletion summary for the given exam. + * + * @param examId the ID of the exam for which the deletion summary should be fetched + * @return the exam deletion summary + */ + public ExamDeletionSummaryDTO getExamDeletionSummary(@NotNull long examId) { + Exam exam = examRepository.findOneWithEagerExercisesGroupsAndStudentExams(examId); + long numberOfBuilds = exam.getExerciseGroups().stream().flatMap(group -> group.getExercises().stream()) + .filter(exercise -> ExerciseType.PROGRAMMING.equals(exercise.getExerciseType())) + .mapToLong(exercise -> buildJobRepository.countBuildJobsByExerciseIds(List.of(exercise.getId()))).sum(); + + Channel channel = channelRepository.findChannelByExamId(examId); + Long conversationId = channel.getId(); + + List postIds = postRepository.findAllByConversationId(conversationId).stream().map(Post::getId).toList(); + long numberOfCommunicationPosts = postIds.size(); + long numberOfAnswerPosts = answerPostRepository.countAnswerPostsByPostIdIn(postIds); + + Set studentExams = exam.getStudentExams(); + long numberRegisteredStudents = studentExams.size(); + + // Boolean.TRUE/Boolean.FALSE are used to handle the case where isStarted/isSubmitted is null + long notStartedExams = studentExams.stream().filter(studentExam -> studentExam.isStarted() == null || !studentExam.isStarted()).count(); + long startedExams = studentExams.stream().filter(studentExam -> Boolean.TRUE.equals(studentExam.isStarted())).count(); + long submittedExams = studentExams.stream().filter(studentExam -> Boolean.TRUE.equals(studentExam.isStarted()) && Boolean.TRUE.equals(studentExam.isSubmitted())).count(); + + return new ExamDeletionSummaryDTO(numberOfBuilds, numberOfCommunicationPosts, numberOfAnswerPosts, numberRegisteredStudents, notStartedExams, startedExams, submittedExams); + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamImportService.java b/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamImportService.java index 92430d5ef7f1..58fcf03cc97a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamImportService.java +++ b/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamImportService.java @@ -291,7 +291,7 @@ private void addExercisesToExerciseGroup(ExerciseGroup exerciseGroupToCopy, Exer Optional exerciseCopied = switch (exerciseToCopy.getExerciseType()) { case MODELING -> { final Optional optionalOriginalModellingExercise = modelingExerciseRepository - .findByIdWithExampleSubmissionsAndResultsAndPlagiarismDetectionConfig(exerciseToCopy.getId()); + .findByIdWithExampleSubmissionsAndResultsAndPlagiarismDetectionConfigAndGradingCriteria(exerciseToCopy.getId()); // We do not want to abort the whole exam import process, we only skip the relevant exercise if (optionalOriginalModellingExercise.isEmpty()) { yield Optional.empty(); diff --git a/src/main/java/de/tum/cit/aet/artemis/exam/web/ExamResource.java b/src/main/java/de/tum/cit/aet/artemis/exam/web/ExamResource.java index 40be2685e58e..8223ba8e54a9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exam/web/ExamResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/exam/web/ExamResource.java @@ -77,6 +77,7 @@ import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastInstructor; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastTutor; +import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInCourse.EnforceAtLeastInstructorInCourse; import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; import de.tum.cit.aet.artemis.core.service.feature.Feature; import de.tum.cit.aet.artemis.core.service.feature.FeatureToggle; @@ -88,6 +89,7 @@ import de.tum.cit.aet.artemis.exam.domain.StudentExam; import de.tum.cit.aet.artemis.exam.domain.SuspiciousSessionsAnalysisOptions; import de.tum.cit.aet.artemis.exam.dto.ExamChecklistDTO; +import de.tum.cit.aet.artemis.exam.dto.ExamDeletionSummaryDTO; import de.tum.cit.aet.artemis.exam.dto.ExamInformationDTO; import de.tum.cit.aet.artemis.exam.dto.ExamScoresDTO; import de.tum.cit.aet.artemis.exam.dto.ExamUserDTO; @@ -1170,13 +1172,13 @@ public ResponseEntity getLatestIndividualEndDateOfExam(@Path } /** - * GET /courses/:courseId/exams/:examId/lockedSubmissions Get locked submissions for exam for user + * GET /courses/:courseId/exams/:examId/locked-submissions Get locked submissions for exam for user * * @param courseId - the id of the course * @param examId - the id of the exam * @return the ResponseEntity with status 200 (OK) and with body the course, or with status 404 (Not Found) */ - @GetMapping("courses/{courseId}/exams/{examId}/lockedSubmissions") + @GetMapping("courses/{courseId}/exams/{examId}/locked-submissions") @EnforceAtLeastInstructor public ResponseEntity> getLockedSubmissionsForExam(@PathVariable Long courseId, @PathVariable Long examId) { log.debug("REST request to get all locked submissions for course : {}", courseId); @@ -1320,4 +1322,19 @@ public ResponseEntity> getAllSuspiciousExamSessio analyzeSessionsIpOutsideOfRange); return ResponseEntity.ok(examSessionService.retrieveAllSuspiciousExamSessionsByExamId(examId, options, Optional.ofNullable(ipSubnet))); } + + /** + * GET /courses/{courseId}/exams/{examId}/deletion-summary : Get a summary of the deletion of an exam. + * + * @param courseId the id of the course + * @param examId the id of the exam + * + * @return the ResponseEntity with status 200 (OK) and with body a summary of the deletion of the exam + */ + @GetMapping("courses/{courseId}/exams/{examId}/deletion-summary") + @EnforceAtLeastInstructorInCourse + public ResponseEntity getDeletionSummary(@PathVariable long courseId, @PathVariable long examId) { + log.debug("REST request to get deletion summary for exam : {}", examId); + return ResponseEntity.ok(examDeletionService.getExamDeletionSummary(examId)); + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/exam/web/ExerciseGroupResource.java b/src/main/java/de/tum/cit/aet/artemis/exam/web/ExerciseGroupResource.java index 2593102ed063..97ebc25f858b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exam/web/ExerciseGroupResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/exam/web/ExerciseGroupResource.java @@ -83,7 +83,7 @@ public ExerciseGroupResource(ExerciseGroupRepository exerciseGroupRepository, Ex } /** - * POST /courses/{courseId}/exams/{examId}/exerciseGroups : Create a new exercise group. + * POST /courses/{courseId}/exams/{examId}/exercise-groups : Create a new exercise group. * * @param courseId the course to which the exercise group belongs to * @param examId the exam to which the exercise group belongs to @@ -92,7 +92,7 @@ public ExerciseGroupResource(ExerciseGroupRepository exerciseGroupRepository, Ex * or with status 400 (Bad Request) if the exerciseGroup has already an ID * @throws URISyntaxException if the Location URI syntax is incorrect */ - @PostMapping("courses/{courseId}/exams/{examId}/exerciseGroups") + @PostMapping("courses/{courseId}/exams/{examId}/exercise-groups") @EnforceAtLeastEditor public ResponseEntity createExerciseGroup(@PathVariable Long courseId, @PathVariable Long examId, @RequestBody ExerciseGroup exerciseGroup) throws URISyntaxException { @@ -117,11 +117,11 @@ public ResponseEntity createExerciseGroup(@PathVariable Long cour Exam savedExam = examRepository.save(examFromDB); ExerciseGroup savedExerciseGroup = savedExam.getExerciseGroups().getLast(); - return ResponseEntity.created(new URI("/api/courses/" + courseId + "/exams/" + examId + "/exerciseGroups/" + savedExerciseGroup.getId())).body(savedExerciseGroup); + return ResponseEntity.created(new URI("/api/courses/" + courseId + "/exams/" + examId + "/exercise-groups/" + savedExerciseGroup.getId())).body(savedExerciseGroup); } /** - * PUT /courses/{courseId}/exams/{examId}/exerciseGroups : Update an existing exercise group. + * PUT /courses/{courseId}/exams/{examId}/exercise-groups : Update an existing exercise group. * * @param courseId the course to which the exercise group belongs to * @param examId the exam to which the exercise group belongs to @@ -129,7 +129,7 @@ public ResponseEntity createExerciseGroup(@PathVariable Long cour * @return the ResponseEntity with status 200 (OK) and with the body of the updated exercise group * @throws URISyntaxException if the Location URI syntax is incorrect */ - @PutMapping("courses/{courseId}/exams/{examId}/exerciseGroups") + @PutMapping("courses/{courseId}/exams/{examId}/exercise-groups") @EnforceAtLeastEditor public ResponseEntity updateExerciseGroup(@PathVariable Long courseId, @PathVariable Long examId, @RequestBody ExerciseGroup updatedExerciseGroup) throws URISyntaxException { @@ -170,14 +170,14 @@ public ResponseEntity> importExerciseGroup(@PathVariable Lon } /** - * GET /courses/{courseId}/exams/{examId}/exerciseGroups/{exerciseGroupId} : Find an exercise group by id. + * GET /courses/{courseId}/exams/{examId}/exercise-groups/{exerciseGroupId} : Find an exercise group by id. * * @param courseId the course to which the exercise group belongs to * @param examId the exam to which the exercise group belongs to * @param exerciseGroupId the id of the exercise group to find * @return the ResponseEntity with status 200 (OK) and with the found exercise group as body */ - @GetMapping("courses/{courseId}/exams/{examId}/exerciseGroups/{exerciseGroupId}") + @GetMapping("courses/{courseId}/exams/{examId}/exercise-groups/{exerciseGroupId}") @EnforceAtLeastEditor public ResponseEntity getExerciseGroup(@PathVariable Long courseId, @PathVariable Long examId, @PathVariable Long exerciseGroupId) { log.debug("REST request to get exercise group : {}", exerciseGroupId); @@ -189,13 +189,13 @@ public ResponseEntity getExerciseGroup(@PathVariable Long courseI } /** - * GET courses/{courseId}/exams/{examId}/exerciseGroups : Get all exercise groups of the given exam + * GET courses/{courseId}/exams/{examId}/exercise-groups : Get all exercise groups of the given exam * * @param courseId the course to which the exercise groups belong to * @param examId the exam to which the exercise groups belong to * @return the ResponseEntity with status 200 (OK) and a list of exercise groups. The list can be empty */ - @GetMapping("courses/{courseId}/exams/{examId}/exerciseGroups") + @GetMapping("courses/{courseId}/exams/{examId}/exercise-groups") @EnforceAtLeastEditor public ResponseEntity> getExerciseGroupsForExam(@PathVariable Long courseId, @PathVariable Long examId) { log.debug("REST request to get all exercise groups for exam : {}", examId); @@ -207,7 +207,7 @@ public ResponseEntity> getExerciseGroupsForExam(@PathVariabl } /** - * DELETE /courses/{courseId}/exams/{examId}/exerciseGroups/{exerciseGroupId} : Delete the exercise group with the given id. + * DELETE /courses/{courseId}/exams/{examId}/exercise-groups/{exerciseGroupId} : Delete the exercise group with the given id. * * @param courseId the course to which the exercise group belongs to * @param examId the exam to which the exercise group belongs to @@ -218,7 +218,7 @@ public ResponseEntity> getExerciseGroupsForExam(@PathVariabl * LocalCI, it does not make sense to keep these artifacts * @return the ResponseEntity with status 200 (OK) */ - @DeleteMapping("courses/{courseId}/exams/{examId}/exerciseGroups/{exerciseGroupId}") + @DeleteMapping("courses/{courseId}/exams/{examId}/exercise-groups/{exerciseGroupId}") @EnforceAtLeastInstructor public ResponseEntity deleteExerciseGroup(@PathVariable Long courseId, @PathVariable Long examId, @PathVariable Long exerciseGroupId, @RequestParam(defaultValue = "true") boolean deleteStudentReposBuildPlans, @RequestParam(defaultValue = "true") boolean deleteBaseReposBuildPlans) { diff --git a/src/main/java/de/tum/cit/aet/artemis/exam/web/admin/AdminExamResource.java b/src/main/java/de/tum/cit/aet/artemis/exam/web/admin/AdminExamResource.java index add9135f8c54..d1f70035e858 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exam/web/admin/AdminExamResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/exam/web/admin/AdminExamResource.java @@ -20,6 +20,7 @@ * REST controller for administrating Exam. */ @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminExamResource { @@ -38,7 +39,6 @@ public AdminExamResource(ExamRepository examRepository) { * @return the ResponseEntity with status 200 (OK) and a list of exams. */ @GetMapping("courses/upcoming-exams") - @EnforceAdmin public ResponseEntity> getCurrentAndUpcomingExams() { log.debug("REST request to get all upcoming exams"); diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java b/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java index 7503427a81fc..b25eb7ab154d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java @@ -562,8 +562,9 @@ public Submission findLatestSubmissionWithRatedResultWithCompletionDate(Particip boolean ratedOrPractice = Boolean.TRUE.equals(result.isRated()) || participation.isPracticeMode(); boolean noProgrammingAndAssessmentOver = !isProgrammingExercise && isAssessmentOver; // For programming exercises we check that the assessment due date has passed (if set) for manual results otherwise we always show the automatic result - boolean programmingAfterAssessmentOrAutomatic = isProgrammingExercise && ((result.isManual() && isAssessmentOver) || result.isAutomatic()); - if (ratedOrPractice && (noProgrammingAndAssessmentOver || programmingAfterAssessmentOrAutomatic)) { + boolean programmingAfterAssessmentOrAutomaticOrAthena = isProgrammingExercise + && ((result.isManual() && isAssessmentOver) || result.isAutomatic() || result.isAthenaBased()); + if (ratedOrPractice && (noProgrammingAndAssessmentOver || programmingAfterAssessmentOrAutomaticOrAthena)) { // take the first found result that fulfills the above requirements // or // take newer results and thus disregard older ones diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Submission.java b/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Submission.java index 326507d47dd4..304c206938b8 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Submission.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Submission.java @@ -162,7 +162,7 @@ public Result getResultForCorrectionRound(int correctionRound) { */ @NotNull private List filterNonAutomaticResults() { - return results.stream().filter(result -> result == null || !(result.isAutomatic() || result.isAthenaAutomatic())).toList(); + return results.stream().filter(result -> result == null || !(result.isAutomatic() || result.isAthenaBased())).toList(); } /** @@ -188,8 +188,7 @@ public boolean hasResultForCorrectionRound(int correctionRound) { */ @JsonIgnore public void removeAutomaticResults() { - this.results = this.results.stream().filter(result -> result == null || !(result.isAutomatic() || result.isAthenaAutomatic())) - .collect(Collectors.toCollection(ArrayList::new)); + this.results = this.results.stream().filter(result -> result == null || !(result.isAutomatic() || result.isAthenaBased())).collect(Collectors.toCollection(ArrayList::new)); } /** @@ -214,7 +213,7 @@ public List getResults() { @JsonIgnore public List getManualResults() { - return results.stream().filter(result -> result != null && !result.isAutomatic() && !result.isAthenaAutomatic()).collect(Collectors.toCollection(ArrayList::new)); + return results.stream().filter(result -> result != null && !result.isAutomatic() && !result.isAthenaBased()).collect(Collectors.toCollection(ArrayList::new)); } /** @@ -224,7 +223,7 @@ public List getManualResults() { */ @JsonIgnore public List getNonAthenaResults() { - return results.stream().filter(result -> result != null && !result.isAthenaAutomatic()).collect(Collectors.toCollection(ArrayList::new)); + return results.stream().filter(result -> result != null && !result.isAthenaBased()).collect(Collectors.toCollection(ArrayList::new)); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java b/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java index 6559c28b9d93..898b4456de07 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java @@ -20,7 +20,6 @@ import jakarta.annotation.Nullable; import jakarta.validation.constraints.NotNull; -import org.apache.velocity.exception.ResourceNotFoundException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -382,7 +381,7 @@ private ResponseEntity handleExerciseFeedbackRequest(Exerc throw new BadRequestAlertException("Not intended for the use in exams", "participation", "preconditions not met"); } if (exercise.getDueDate() != null && now().isAfter(exercise.getDueDate())) { - throw new BadRequestAlertException("The due date is over", "participation", "preconditions not met"); + throw new BadRequestAlertException("The due date is over", "participation", "feedbackRequestAfterDueDate", true); } if (exercise instanceof ProgrammingExercise) { ((ProgrammingExercise) exercise).validateSettingsForFeedbackRequest(); @@ -393,7 +392,7 @@ private ResponseEntity handleExerciseFeedbackRequest(Exerc StudentParticipation participation = (exercise instanceof ProgrammingExercise) ? programmingExerciseParticipationService.findStudentParticipationByExerciseAndStudentId(exercise, principal.getName()) : studentParticipationRepository.findByExerciseIdAndStudentLogin(exercise.getId(), principal.getName()) - .orElseThrow(() -> new ResourceNotFoundException("Participation not found")); + .orElseThrow(() -> new BadRequestAlertException("Submission not found", "participation", "noSubmissionExists", true)); checkAccessPermissionOwner(participation, user); participation = studentParticipationRepository.findByIdWithResultsElseThrow(participation.getId()); @@ -406,15 +405,14 @@ private ResponseEntity handleExerciseFeedbackRequest(Exerc } else if (exercise instanceof ProgrammingExercise) { if (participation.findLatestLegalResult() == null) { - throw new BadRequestAlertException("User has not reached the conditions to submit a feedback request", "participation", "preconditions not met"); + throw new BadRequestAlertException("You need to submit at least once and have the build results", "participation", "noSubmissionExists", true); } } // Check if feedback has already been requested - var currentDate = now(); - var participationIndividualDueDate = participation.getIndividualDueDate(); - if (participationIndividualDueDate != null && currentDate.isAfter(participationIndividualDueDate)) { - throw new BadRequestAlertException("Request has already been sent", "participation", "already sent"); + var latestResult = participation.findLatestResult(); + if (latestResult != null && latestResult.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA && latestResult.getCompletionDate().isAfter(now())) { + throw new BadRequestAlertException("Request has already been sent", "participation", "feedbackRequestAlreadySent", true); } // Process feedback request @@ -718,7 +716,7 @@ public ResponseEntity> getAllParticipationsForCourse( * @param participationId the participationId of the participation to retrieve * @return the ResponseEntity with status 200 (OK) and with body the participation, or with status 404 (Not Found) */ - @GetMapping("participations/{participationId}/withLatestResult") + @GetMapping("participations/{participationId}/with-latest-result") @EnforceAtLeastStudent public ResponseEntity getParticipationWithLatestResult(@PathVariable Long participationId) { log.debug("REST request to get Participation : {}", participationId); @@ -756,7 +754,7 @@ public ResponseEntity getParticipationForCurrentUser(@Path * @param participationId The participationId of the participation * @return The latest build artifact (JAR/WAR) for the participation */ - @GetMapping("participations/{participationId}/buildArtifact") + @GetMapping("participations/{participationId}/build-artifact") @EnforceAtLeastStudent public ResponseEntity getParticipationBuildArtifact(@PathVariable Long participationId) { log.debug("REST request to get Participation build artifact: {}", participationId); @@ -931,14 +929,14 @@ private ResponseEntity deleteParticipation(StudentParticipation participat } /** - * DELETE /participations/:participationId : remove the build plan of the ProgrammingExerciseStudentParticipation of the "participationId". + * DELETE /participations/:participationId/cleanup-build-plan : remove the build plan of the ProgrammingExerciseStudentParticipation of the "participationId". * This only works for programming exercises. * * @param participationId the participationId of the ProgrammingExerciseStudentParticipation for which the build plan should be removed * @param principal The identity of the user accessing this resource * @return the ResponseEntity with status 200 (OK) */ - @PutMapping("participations/{participationId}/cleanupBuildPlan") + @PutMapping("participations/{participationId}/cleanup-build-plan") @EnforceAtLeastInstructor @FeatureToggle(Feature.ProgrammingExercises) public ResponseEntity cleanupBuildPlan(@PathVariable Long participationId, Principal principal) { diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/web/admin/AdminExerciseResource.java b/src/main/java/de/tum/cit/aet/artemis/exercise/web/admin/AdminExerciseResource.java index 306ad29ddd4f..76546cbe6289 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/web/admin/AdminExerciseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/web/admin/AdminExerciseResource.java @@ -20,6 +20,7 @@ * REST controller for administrating Exercise. */ @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminExerciseResource { @@ -38,7 +39,6 @@ public AdminExerciseResource(ExerciseRepository exerciseRepository) { * @return the ResponseEntity with status 200 (OK) and a list of exercises. */ @GetMapping("exercises/upcoming") - @EnforceAdmin public ResponseEntity> getUpcomingExercises() { log.debug("REST request to get all upcoming exercises"); Set upcomingExercises = exerciseRepository.findAllExercisesWithCurrentOrUpcomingDueDate(); diff --git a/src/main/java/de/tum/cit/aet/artemis/fileupload/repository/FileUploadExerciseRepository.java b/src/main/java/de/tum/cit/aet/artemis/fileupload/repository/FileUploadExerciseRepository.java index 4a0a53761fe0..376a70db4c7e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/fileupload/repository/FileUploadExerciseRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/fileupload/repository/FileUploadExerciseRepository.java @@ -5,9 +5,11 @@ import java.util.List; import java.util.Optional; +import java.util.Set; import jakarta.validation.constraints.NotNull; +import org.hibernate.NonUniqueResultException; import org.springframework.context.annotation.Profile; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; @@ -15,6 +17,7 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import de.tum.cit.aet.artemis.core.exception.NoUniqueQueryException; import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; import de.tum.cit.aet.artemis.fileupload.domain.FileUploadExercise; @@ -38,8 +41,41 @@ public interface FileUploadExerciseRepository extends ArtemisJpaRepository findWithEagerTeamAssignmentConfigAndCategoriesAndCompetenciesById(Long exerciseId); + @Query(""" + SELECT f + FROM FileUploadExercise f + LEFT JOIN FETCH f.competencies + WHERE f.title = :title + AND f.course.id = :courseId + """) + Set findAllWithCompetenciesByTitleAndCourseId(@Param("title") String title, @Param("courseId") long courseId) throws NonUniqueResultException; + + /** + * Finds a file upload exercise by its title and course id and throws a NoUniqueQueryException if multiple exercises are found. + * + * @param title the title of the exercise + * @param courseId the id of the course + * @return the exercise with the given title and course id + * @throws NoUniqueQueryException if multiple exercises are found with the same title + */ + default Optional findUniqueWithCompetenciesByTitleAndCourseId(String title, long courseId) throws NoUniqueQueryException { + Set allExercises = findAllWithCompetenciesByTitleAndCourseId(title, courseId); + if (allExercises.size() > 1) { + throw new NoUniqueQueryException("Found multiple exercises with title " + title + " in course with id " + courseId); + } + return allExercises.stream().findFirst(); + } + @NotNull default FileUploadExercise findWithEagerCompetenciesByIdElseThrow(Long exerciseId) { return getValueElseThrow(findWithEagerCompetenciesById(exerciseId), exerciseId); } + + @EntityGraph(type = LOAD, attributePaths = { "gradingCriteria" }) + Optional findWithGradingCriteriaById(Long exerciseId); + + @NotNull + default FileUploadExercise findWithGradingCriteriaByIdElseThrow(Long exerciseId) { + return getValueElseThrow(findWithGradingCriteriaById(exerciseId), exerciseId); + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/IrisTemplate.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/IrisTemplate.java deleted file mode 100644 index e1b486a34cbf..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/IrisTemplate.java +++ /dev/null @@ -1,65 +0,0 @@ -package de.tum.cit.aet.artemis.iris.domain; - -import java.util.Objects; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; - -import org.hibernate.annotations.Cache; -import org.hibernate.annotations.CacheConcurrencyStrategy; - -import com.fasterxml.jackson.annotation.JsonInclude; - -import de.tum.cit.aet.artemis.core.domain.DomainObject; - -/** - * An IrisTemplate represents a handlebars template for Iris. - * It is sent to the Iris Python server to generate a response. - */ -@Entity -@Table(name = "iris_template") -@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) -@JsonInclude(JsonInclude.Include.NON_EMPTY) -public class IrisTemplate extends DomainObject { - - @Column(name = "content", columnDefinition = "LONGTEXT") - private String content; - - /** - * Empty constructor required for Hibernate and Jackson. - */ - public IrisTemplate() { - } - - /** - * Create a new IrisTemplate with content. - * - * @param content the content of the template - */ - public IrisTemplate(String content) { - this.content = content; - } - - public String getContent() { - return content; - } - - public void setContent(String template) { - this.content = template; - } - - @Override - public boolean equals(Object other) { - if (!super.equals(other)) { - return false; - } - IrisTemplate template = (IrisTemplate) other; - return Objects.equals(content, template.content); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), content); - } -} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/session/IrisHestiaSession.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/session/IrisHestiaSession.java deleted file mode 100644 index f23603711cf5..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/session/IrisHestiaSession.java +++ /dev/null @@ -1,37 +0,0 @@ -package de.tum.cit.aet.artemis.iris.domain.session; - -import jakarta.persistence.DiscriminatorValue; -import jakarta.persistence.Entity; -import jakarta.persistence.ManyToOne; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; - -import de.tum.cit.aet.artemis.programming.domain.hestia.CodeHint; - -/** - * An Iris session for a hestia code hint. - * Currently used to generate descriptions for code hints. - */ -@Entity -@DiscriminatorValue("HESTIA") -@JsonInclude(JsonInclude.Include.NON_EMPTY) -public class IrisHestiaSession extends IrisSession { - - @ManyToOne - @JsonIgnore - private CodeHint codeHint; - - public CodeHint getCodeHint() { - return codeHint; - } - - public void setCodeHint(CodeHint codeHint) { - this.codeHint = codeHint; - } - - @Override - public String toString() { - return "IrisHestiaSession{" + "id=" + getId() + ", codeHint=" + (codeHint == null ? "null" : codeHint.getId()) + '}'; - } -} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/session/IrisSession.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/session/IrisSession.java index 3e6240a383de..13a4bd6f8b2b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/session/IrisSession.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/session/IrisSession.java @@ -28,7 +28,7 @@ /** * An IrisSession represents a list of messages of Artemis, a user, and an LLM. - * See {@link IrisExerciseChatSession} and {@link IrisHestiaSession} for concrete implementations. + * See {@link IrisExerciseChatSession} and {@link IrisCourseChatSession} for concrete implementations. */ @Entity @Table(name = "iris_session") @@ -40,7 +40,6 @@ @JsonSubTypes({ @JsonSubTypes.Type(value = IrisExerciseChatSession.class, name = "chat"), // TODO: Legacy. Should ideally be "exercise_chat" @JsonSubTypes.Type(value = IrisCourseChatSession.class, name = "course_chat"), - @JsonSubTypes.Type(value = IrisHestiaSession.class, name = "hestia"), }) // @formatter:on @JsonInclude(JsonInclude.Include.NON_EMPTY) diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/session/IrisTextExerciseChatSession.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/session/IrisTextExerciseChatSession.java new file mode 100644 index 000000000000..071c4c5ed325 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/session/IrisTextExerciseChatSession.java @@ -0,0 +1,48 @@ +package de.tum.cit.aet.artemis.iris.domain.session; + +import java.util.Optional; + +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.text.domain.TextExercise; + +/** + * An IrisTextExerciseChatSession represents a conversation between a user and an LLM in the context of a text exercise. + * This is used for students receiving tutor assistance from Iris while working on a text exercise. + */ +@Entity +@DiscriminatorValue("TEXT_EXERCISE_CHAT") +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class IrisTextExerciseChatSession extends IrisChatSession { + + @ManyToOne + @JsonIgnore + private TextExercise exercise; + + public IrisTextExerciseChatSession() { + } + + public IrisTextExerciseChatSession(TextExercise exercise, User user) { + super(user); + this.exercise = exercise; + } + + public TextExercise getExercise() { + return exercise; + } + + public void setExercise(TextExercise exercise) { + this.exercise = exercise; + } + + @Override + public String toString() { + return "IrisTextExerciseChatSession{" + "user=" + Optional.ofNullable(getUser()).map(User::getLogin).orElse("null") + "," + "exercise=" + exercise + '}'; + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisChatSubSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisChatSubSettings.java index 4305461b71cf..bf2851ae7979 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisChatSubSettings.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisChatSubSettings.java @@ -1,31 +1,21 @@ package de.tum.cit.aet.artemis.iris.domain.settings; import jakarta.annotation.Nullable; -import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.OneToOne; import com.fasterxml.jackson.annotation.JsonInclude; -import de.tum.cit.aet.artemis.iris.domain.IrisTemplate; - /** * An {@link IrisSubSettings} implementation for chat settings. * Chat settings notably provide settings for the rate limit. - * Chat settings provide a single {@link IrisTemplate} for the chat messages. */ @Entity @DiscriminatorValue("CHAT") @JsonInclude(JsonInclude.Include.NON_EMPTY) public class IrisChatSubSettings extends IrisSubSettings { - @Nullable - @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) - private IrisTemplate template; - @Nullable @Column(name = "rate_limit") private Integer rateLimit; @@ -34,15 +24,6 @@ public class IrisChatSubSettings extends IrisSubSettings { @Column(name = "rate_limit_timeframe_hours") private Integer rateLimitTimeframeHours; - @Nullable - public IrisTemplate getTemplate() { - return template; - } - - public void setTemplate(@Nullable IrisTemplate template) { - this.template = template; - } - @Nullable public Integer getRateLimit() { return rateLimit; diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisCompetencyGenerationSubSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisCompetencyGenerationSubSettings.java index f68ae30d4b53..b8447b1bb378 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisCompetencyGenerationSubSettings.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisCompetencyGenerationSubSettings.java @@ -1,36 +1,16 @@ package de.tum.cit.aet.artemis.iris.domain.settings; -import jakarta.annotation.Nullable; -import jakarta.persistence.CascadeType; import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.OneToOne; import com.fasterxml.jackson.annotation.JsonInclude; -import de.tum.cit.aet.artemis.iris.domain.IrisTemplate; - /** * An {@link IrisSubSettings} implementation for the settings for competency generation. - * CompetencyGeneration settings provide a single {@link IrisTemplate} */ @Entity @DiscriminatorValue("COMPETENCY_GENERATION") @JsonInclude(JsonInclude.Include.NON_EMPTY) public class IrisCompetencyGenerationSubSettings extends IrisSubSettings { - @Nullable - @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) - private IrisTemplate template; - - @Nullable - public IrisTemplate getTemplate() { - return template; - } - - public void setTemplate(@Nullable IrisTemplate template) { - this.template = template; - } - } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisCourseSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisCourseSettings.java index 2354ffd3c142..fce389a7b95f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisCourseSettings.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisCourseSettings.java @@ -29,22 +29,17 @@ public class IrisCourseSettings extends IrisSettings { private IrisChatSubSettings irisChatSettings; @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) - @JoinColumn(name = "iris_lecture_ingestion_settings_id") - private IrisLectureIngestionSubSettings irisLectureIngestionSettings; + @JoinColumn(name = "iris_text_exercise_chat_settings_id") + private IrisTextExerciseChatSubSettings irisTextExerciseChatSettings; @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) - @JoinColumn(name = "iris_hestia_settings_id") - private IrisHestiaSubSettings irisHestiaSettings; + @JoinColumn(name = "iris_lecture_ingestion_settings_id") + private IrisLectureIngestionSubSettings irisLectureIngestionSettings; @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, optional = false) @JoinColumn(name = "iris_competency_generation_settings_id") private IrisCompetencyGenerationSubSettings irisCompetencyGenerationSettings; - @Override - public boolean isValid() { - return course != null; - } - public Course getCourse() { return course; } @@ -74,13 +69,13 @@ public void setIrisChatSettings(IrisChatSubSettings irisChatSettings) { } @Override - public IrisHestiaSubSettings getIrisHestiaSettings() { - return irisHestiaSettings; + public IrisTextExerciseChatSubSettings getIrisTextExerciseChatSettings() { + return irisTextExerciseChatSettings; } @Override - public void setIrisHestiaSettings(IrisHestiaSubSettings irisHestiaSettings) { - this.irisHestiaSettings = irisHestiaSettings; + public void setIrisTextExerciseChatSettings(IrisTextExerciseChatSubSettings irisTextExerciseChatSettings) { + this.irisTextExerciseChatSettings = irisTextExerciseChatSettings; } @Override diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisExerciseSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisExerciseSettings.java index 410bbde1954c..ba095a018808 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisExerciseSettings.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisExerciseSettings.java @@ -28,10 +28,9 @@ public class IrisExerciseSettings extends IrisSettings { @JoinColumn(name = "iris_chat_settings_id") private IrisChatSubSettings irisChatSettings; - @Override - public boolean isValid() { - return exercise != null; - } + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) + @JoinColumn(name = "iris_text_exercise_chat_settings_id") + private IrisTextExerciseChatSubSettings irisTextExerciseChatSettings; public Exercise getExercise() { return exercise; @@ -61,13 +60,13 @@ public void setIrisChatSettings(IrisChatSubSettings irisChatSettings) { } @Override - public IrisHestiaSubSettings getIrisHestiaSettings() { - return null; + public IrisTextExerciseChatSubSettings getIrisTextExerciseChatSettings() { + return irisTextExerciseChatSettings; } @Override - public void setIrisHestiaSettings(IrisHestiaSubSettings irisHestiaSettings) { - + public void setIrisTextExerciseChatSettings(IrisTextExerciseChatSubSettings irisTextExerciseChatSettings) { + this.irisTextExerciseChatSettings = irisTextExerciseChatSettings; } @Override diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisGlobalSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisGlobalSettings.java index 0ae60c36edd8..ddb156da0038 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisGlobalSettings.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisGlobalSettings.java @@ -1,15 +1,12 @@ package de.tum.cit.aet.artemis.iris.domain.settings; import jakarta.persistence.CascadeType; -import jakarta.persistence.Column; import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToOne; -import org.hibernate.Hibernate; - import com.fasterxml.jackson.annotation.JsonInclude; /** @@ -22,89 +19,22 @@ @JsonInclude(JsonInclude.Include.NON_EMPTY) public class IrisGlobalSettings extends IrisSettings { - @Column(name = "current_version") - private int currentVersion; - - @Column(name = "enable_auto_update_chat") - private boolean enableAutoUpdateChat; - - @Column(name = "enable_auto_update_hestia") - private boolean enableAutoUpdateHestia; - - @Column(name = "enable_auto_update_lecture_ingestion") - private boolean enableAutoUpdateLectureIngestion; - - @Column(name = "enable_auto_update_competency_generation") - private boolean enableAutoUpdateCompetencyGeneration; - @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, optional = false) @JoinColumn(name = "iris_chat_settings_id") private IrisChatSubSettings irisChatSettings; @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, optional = false) - @JoinColumn(name = "iris_lecture_ingestion_settings_id") - private IrisLectureIngestionSubSettings irisLectureIngestionSettings; + @JoinColumn(name = "iris_text_exercise_chat_settings_id") + private IrisTextExerciseChatSubSettings irisTextExerciseChatSettings; @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, optional = false) - @JoinColumn(name = "iris_hestia_settings_id") - private IrisHestiaSubSettings irisHestiaSettings; + @JoinColumn(name = "iris_lecture_ingestion_settings_id") + private IrisLectureIngestionSubSettings irisLectureIngestionSettings; @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, optional = false) @JoinColumn(name = "iris_competency_generation_settings_id") private IrisCompetencyGenerationSubSettings irisCompetencyGenerationSettings; - @Override - public boolean isValid() { - var chatSettingsValid = !Hibernate.isInitialized(irisChatSettings) || irisChatSettings == null - || (irisChatSettings.getTemplate() != null && irisChatSettings.getTemplate().getContent() != null && !irisChatSettings.getTemplate().getContent().isEmpty()); - var hestiaSettingsValid = !Hibernate.isInitialized(irisHestiaSettings) || irisHestiaSettings == null - || (irisHestiaSettings.getTemplate() != null && irisHestiaSettings.getTemplate().getContent() != null && !irisHestiaSettings.getTemplate().getContent().isEmpty()); - var competencyGenerationSettingsValid = !Hibernate.isInitialized(irisCompetencyGenerationSettings) || irisCompetencyGenerationSettings == null - || (irisCompetencyGenerationSettings.getTemplate() != null && irisCompetencyGenerationSettings.getTemplate().getContent() != null - && !irisCompetencyGenerationSettings.getTemplate().getContent().isEmpty()); - return chatSettingsValid && hestiaSettingsValid && competencyGenerationSettingsValid; - } - - public int getCurrentVersion() { - return currentVersion; - } - - public void setCurrentVersion(int currentVersion) { - this.currentVersion = currentVersion; - } - - public boolean isEnableAutoUpdateChat() { - return enableAutoUpdateChat; - } - - public void setEnableAutoUpdateChat(boolean enableAutoUpdateChat) { - this.enableAutoUpdateChat = enableAutoUpdateChat; - } - - public boolean isEnableAutoUpdateLectureIngestion() { - return enableAutoUpdateLectureIngestion; - } - - public void setEnableAutoUpdateLectureIngestion(boolean enableAutoUpdateLectureIngestion) { - this.enableAutoUpdateLectureIngestion = enableAutoUpdateLectureIngestion; - } - - public boolean isEnableAutoUpdateHestia() { - return enableAutoUpdateHestia; - } - - public void setEnableAutoUpdateHestia(boolean enableAutoUpdateHestia) { - this.enableAutoUpdateHestia = enableAutoUpdateHestia; - } - - public boolean isEnableAutoUpdateCompetencyGeneration() { - return enableAutoUpdateCompetencyGeneration; - } - - public void setEnableAutoUpdateCompetencyGeneration(boolean enableAutoUpdateCompetencyGeneration) { - this.enableAutoUpdateCompetencyGeneration = enableAutoUpdateCompetencyGeneration; - } - @Override public IrisLectureIngestionSubSettings getIrisLectureIngestionSettings() { return irisLectureIngestionSettings; @@ -126,13 +56,13 @@ public void setIrisChatSettings(IrisChatSubSettings irisChatSettings) { } @Override - public IrisHestiaSubSettings getIrisHestiaSettings() { - return irisHestiaSettings; + public IrisTextExerciseChatSubSettings getIrisTextExerciseChatSettings() { + return irisTextExerciseChatSettings; } @Override - public void setIrisHestiaSettings(IrisHestiaSubSettings irisHestiaSettings) { - this.irisHestiaSettings = irisHestiaSettings; + public void setIrisTextExerciseChatSettings(IrisTextExerciseChatSubSettings irisTextExerciseChatSettings) { + this.irisTextExerciseChatSettings = irisTextExerciseChatSettings; } @Override diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisHestiaSubSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisHestiaSubSettings.java deleted file mode 100644 index 1c478a8ccfbe..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisHestiaSubSettings.java +++ /dev/null @@ -1,35 +0,0 @@ -package de.tum.cit.aet.artemis.iris.domain.settings; - -import jakarta.annotation.Nullable; -import jakarta.persistence.CascadeType; -import jakarta.persistence.DiscriminatorValue; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.OneToOne; - -import com.fasterxml.jackson.annotation.JsonInclude; - -import de.tum.cit.aet.artemis.iris.domain.IrisTemplate; - -/** - * An {@link IrisSubSettings} implementation for the Hestia integration settings. - * Hestia settings provide a single {@link IrisTemplate} for the hestia code hint generation requests. - */ -@Entity -@DiscriminatorValue("HESTIA") -@JsonInclude(JsonInclude.Include.NON_EMPTY) -public class IrisHestiaSubSettings extends IrisSubSettings { - - @Nullable - @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) - private IrisTemplate template; - - @Nullable - public IrisTemplate getTemplate() { - return template; - } - - public void setTemplate(@Nullable IrisTemplate template) { - this.template = template; - } -} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisModelListConverter.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisListConverter.java similarity index 88% rename from src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisModelListConverter.java rename to src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisListConverter.java index 938ce5dae0c7..be4cf5199a0b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisModelListConverter.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisListConverter.java @@ -9,7 +9,7 @@ import jakarta.persistence.Converter; @Converter -public class IrisModelListConverter implements AttributeConverter, String> { +public class IrisListConverter implements AttributeConverter, String> { @Override public String convertToDatabaseColumn(SortedSet type) { diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSettings.java index 5ca715a2f688..61b2912d5cf6 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSettings.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSettings.java @@ -45,17 +45,15 @@ public abstract class IrisSettings extends DomainObject { public abstract void setIrisChatSettings(IrisChatSubSettings irisChatSettings); - public abstract IrisLectureIngestionSubSettings getIrisLectureIngestionSettings(); + public abstract IrisTextExerciseChatSubSettings getIrisTextExerciseChatSettings(); - public abstract void setIrisLectureIngestionSettings(IrisLectureIngestionSubSettings irisLectureIngestionSettings); + public abstract void setIrisTextExerciseChatSettings(IrisTextExerciseChatSubSettings irisTextExerciseChatSettings); - public abstract IrisHestiaSubSettings getIrisHestiaSettings(); + public abstract IrisLectureIngestionSubSettings getIrisLectureIngestionSettings(); - public abstract void setIrisHestiaSettings(IrisHestiaSubSettings irisHestiaSettings); + public abstract void setIrisLectureIngestionSettings(IrisLectureIngestionSubSettings irisLectureIngestionSettings); public abstract IrisCompetencyGenerationSubSettings getIrisCompetencyGenerationSettings(); public abstract void setIrisCompetencyGenerationSettings(IrisCompetencyGenerationSubSettings irisCompetencyGenerationSubSettings); - - public abstract boolean isValid(); } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSubSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSubSettings.java index 16588cf448a5..c9fc576311db 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSubSettings.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSubSettings.java @@ -26,7 +26,6 @@ * IrisSubSettings is an abstract super class for the specific sub settings types. * Sub Settings are settings for a specific feature of Iris. * {@link IrisChatSubSettings} are used to specify settings for the chat feature. - * {@link IrisHestiaSubSettings} are used to specify settings for the Hestia integration. * {@link IrisCompetencyGenerationSubSettings} are used to specify settings for the competency generation feature. *

    * Also see {@link de.tum.cit.aet.artemis.iris.service.settings.IrisSettingsService} for more information. @@ -40,8 +39,8 @@ // @formatter:off @JsonSubTypes({ @JsonSubTypes.Type(value = IrisChatSubSettings.class, name = "chat"), + @JsonSubTypes.Type(value = IrisTextExerciseChatSubSettings.class, name = "text-exercise-chat"), @JsonSubTypes.Type(value = IrisLectureIngestionSubSettings.class, name = "lecture-ingestion"), - @JsonSubTypes.Type(value = IrisHestiaSubSettings.class, name = "hestia"), @JsonSubTypes.Type(value = IrisCompetencyGenerationSubSettings.class, name = "competency-generation") }) // @formatter:on @@ -51,13 +50,12 @@ public abstract class IrisSubSettings extends DomainObject { @Column(name = "enabled") private boolean enabled = false; - @Column(name = "allowed_models") - @Convert(converter = IrisModelListConverter.class) - private SortedSet allowedModels = new TreeSet<>(); + @Column(name = "allowed_variants", nullable = false) + @Convert(converter = IrisListConverter.class) + private SortedSet allowedVariants = new TreeSet<>(); - @Nullable - @Column(name = "preferred_model") - private String preferredModel; + @Column(name = "selected_variant", nullable = false) + private String selectedVariant; public boolean isEnabled() { return enabled; @@ -67,20 +65,20 @@ public void setEnabled(boolean enabled) { this.enabled = enabled; } - public SortedSet getAllowedModels() { - return allowedModels; + public SortedSet getAllowedVariants() { + return allowedVariants; } - public void setAllowedModels(SortedSet allowedModels) { - this.allowedModels = allowedModels; + public void setAllowedVariants(SortedSet allowedVariants) { + this.allowedVariants = allowedVariants; } @Nullable - public String getPreferredModel() { - return preferredModel; + public String getSelectedVariant() { + return selectedVariant; } - public void setPreferredModel(@Nullable String preferredModel) { - this.preferredModel = preferredModel; + public void setSelectedVariant(@Nullable String selectedVariant) { + this.selectedVariant = selectedVariant; } } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSubSettingsType.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSubSettingsType.java index d938134f4555..dafdd1edcfb9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSubSettingsType.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSubSettingsType.java @@ -1,5 +1,6 @@ package de.tum.cit.aet.artemis.iris.domain.settings; public enum IrisSubSettingsType { - CHAT, HESTIA, COMPETENCY_GENERATION, LECTURE_INGESTION + CHAT, // TODO: Split into PROGRAMMING_EXERCISE_CHAT and COURSE_CHAT + TEXT_EXERCISE_CHAT, COMPETENCY_GENERATION, LECTURE_INGESTION } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisTextExerciseChatSubSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisTextExerciseChatSubSettings.java new file mode 100644 index 000000000000..ed090bbe892a --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisTextExerciseChatSubSettings.java @@ -0,0 +1,44 @@ +package de.tum.cit.aet.artemis.iris.domain.settings; + +import jakarta.annotation.Nullable; +import jakarta.persistence.Column; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * An {@link IrisSubSettings} implementation for the settings for the chat in a text exercise. + */ +@Entity +@DiscriminatorValue("TEXT_EXERCISE_CHAT") +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class IrisTextExerciseChatSubSettings extends IrisSubSettings { + + @Nullable + @Column(name = "rate_limit") + private Integer rateLimit; + + @Nullable + @Column(name = "rate_limit_timeframe_hours") + private Integer rateLimitTimeframeHours; + + @Nullable + public Integer getRateLimit() { + return rateLimit; + } + + public void setRateLimit(@Nullable Integer rateLimit) { + this.rateLimit = rateLimit; + } + + @Nullable + public Integer getRateLimitTimeframeHours() { + return rateLimitTimeframeHours; + } + + public void setRateLimitTimeframeHours(@Nullable Integer rateLimitTimeframeHours) { + this.rateLimitTimeframeHours = rateLimitTimeframeHours; + } + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedChatSubSettingsDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedChatSubSettingsDTO.java index 72d8e599ed70..c5589e824507 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedChatSubSettingsDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedChatSubSettingsDTO.java @@ -6,10 +6,8 @@ import com.fasterxml.jackson.annotation.JsonInclude; -import de.tum.cit.aet.artemis.iris.domain.IrisTemplate; - @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record IrisCombinedChatSubSettingsDTO(boolean enabled, Integer rateLimit, Integer rateLimitTimeframeHours, @Nullable Set allowedModels, - @Nullable String preferredModel, @Nullable IrisTemplate template) { +public record IrisCombinedChatSubSettingsDTO(boolean enabled, Integer rateLimit, Integer rateLimitTimeframeHours, @Nullable Set allowedVariants, + @Nullable String selectedVariant) { } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedCompetencyGenerationSubSettingsDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedCompetencyGenerationSubSettingsDTO.java index 18ffcbc17b50..414b422e0f64 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedCompetencyGenerationSubSettingsDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedCompetencyGenerationSubSettingsDTO.java @@ -6,9 +6,6 @@ import com.fasterxml.jackson.annotation.JsonInclude; -import de.tum.cit.aet.artemis.iris.domain.IrisTemplate; - @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record IrisCombinedCompetencyGenerationSubSettingsDTO(boolean enabled, @Nullable Set allowedModels, @Nullable String preferredModel, - @Nullable IrisTemplate template) { +public record IrisCombinedCompetencyGenerationSubSettingsDTO(boolean enabled, @Nullable Set allowedVariants, @Nullable String selectedVariant) { } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedHestiaSubSettingsDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedHestiaSubSettingsDTO.java deleted file mode 100644 index c70ce4825a92..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedHestiaSubSettingsDTO.java +++ /dev/null @@ -1,13 +0,0 @@ -package de.tum.cit.aet.artemis.iris.dto; - -import java.util.Set; - -import jakarta.annotation.Nullable; - -import com.fasterxml.jackson.annotation.JsonInclude; - -import de.tum.cit.aet.artemis.iris.domain.IrisTemplate; - -@JsonInclude(JsonInclude.Include.NON_EMPTY) -public record IrisCombinedHestiaSubSettingsDTO(boolean enabled, @Nullable Set allowedModels, @Nullable String preferredModel, @Nullable IrisTemplate template) { -} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedSettingsDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedSettingsDTO.java index 9353757c782e..b05645603dbe 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedSettingsDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedSettingsDTO.java @@ -2,7 +2,12 @@ import com.fasterxml.jackson.annotation.JsonInclude; +// @formatter:off @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record IrisCombinedSettingsDTO(IrisCombinedChatSubSettingsDTO irisChatSettings, IrisCombinedLectureIngestionSubSettingsDTO irisLectureIngestionSettings, - IrisCombinedHestiaSubSettingsDTO irisHestiaSettings, IrisCombinedCompetencyGenerationSubSettingsDTO irisCompetencyGenerationSettings) { -} +public record IrisCombinedSettingsDTO( + IrisCombinedChatSubSettingsDTO irisChatSettings, + IrisCombinedTextExerciseChatSubSettingsDTO irisTextExerciseChatSettings, + IrisCombinedLectureIngestionSubSettingsDTO irisLectureIngestionSettings, + IrisCombinedCompetencyGenerationSubSettingsDTO irisCompetencyGenerationSettings +) {} +// @formatter:on diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedTextExerciseChatSubSettingsDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedTextExerciseChatSubSettingsDTO.java new file mode 100644 index 000000000000..4f02a2d87720 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedTextExerciseChatSubSettingsDTO.java @@ -0,0 +1,13 @@ +package de.tum.cit.aet.artemis.iris.dto; + +import java.util.Set; + +import jakarta.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record IrisCombinedTextExerciseChatSubSettingsDTO(boolean enabled, Integer rateLimit, Integer rateLimitTimeframeHours, @Nullable Set allowedVariants, + @Nullable String selectedVariant) { + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisHestiaSessionRepository.java b/src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisHestiaSessionRepository.java deleted file mode 100644 index 22a14bd98bd7..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisHestiaSessionRepository.java +++ /dev/null @@ -1,28 +0,0 @@ -package de.tum.cit.aet.artemis.iris.repository; - -import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_IRIS; - -import java.util.List; - -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Repository; - -import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; -import de.tum.cit.aet.artemis.iris.domain.session.IrisHestiaSession; - -/** - * Repository interface for managing {@link IrisHestiaSession} entities. - * Provides custom queries for finding hestia sessions based on different criteria. - */ -@Repository -@Profile(PROFILE_IRIS) -public interface IrisHestiaSessionRepository extends ArtemisJpaRepository { - - /** - * Finds a list of {@link IrisHestiaSession} based on the exercise and user IDs. - * - * @param codeHintId The ID of the code hint. - * @return A list of hestia sessions sorted by creation date in descending order. - */ - List findByCodeHintIdOrderByCreationDateDesc(Long codeHintId); -} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisSettingsRepository.java b/src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisSettingsRepository.java index 8c4ffd56068e..c9e8eafba618 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisSettingsRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisSettingsRepository.java @@ -28,8 +28,8 @@ public interface IrisSettingsRepository extends ArtemisJpaRepository findAllGlobalSettings(); @@ -42,8 +42,8 @@ default IrisGlobalSettings findGlobalSettingsElseThrow() { SELECT irisSettings FROM IrisCourseSettings irisSettings LEFT JOIN FETCH irisSettings.irisChatSettings + LEFT JOIN FETCH irisSettings.irisTextExerciseChatSettings LEFT JOIN FETCH irisSettings.irisLectureIngestionSettings - LEFT JOIN FETCH irisSettings.irisHestiaSettings LEFT JOIN FETCH irisSettings.irisCompetencyGenerationSettings WHERE irisSettings.course.id = :courseId """) @@ -59,6 +59,7 @@ default IrisGlobalSettings findGlobalSettingsElseThrow() { SELECT irisSettings FROM IrisExerciseSettings irisSettings LEFT JOIN FETCH irisSettings.irisChatSettings + LEFT JOIN FETCH irisSettings.irisTextExerciseChatSettings WHERE irisSettings.exercise.id = :exerciseId """) Optional findExerciseSettings(@Param("exerciseId") long exerciseId); diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisTemplateRepository.java b/src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisTemplateRepository.java deleted file mode 100644 index 2b1a930d7aef..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisTemplateRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package de.tum.cit.aet.artemis.iris.repository; - -import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; -import de.tum.cit.aet.artemis.iris.domain.IrisTemplate; - -/** - * Spring Data repository for the IrisTemplate entity. - */ -public interface IrisTemplateRepository extends ArtemisJpaRepository { - -} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisTextExerciseChatSessionRepository.java b/src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisTextExerciseChatSessionRepository.java new file mode 100644 index 000000000000..a8f76c5ff679 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisTextExerciseChatSessionRepository.java @@ -0,0 +1,90 @@ +package de.tum.cit.aet.artemis.iris.repository; + +import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.LOAD; + +import java.util.Collections; +import java.util.List; + +import jakarta.validation.constraints.NotNull; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import de.tum.cit.aet.artemis.core.domain.DomainObject; +import de.tum.cit.aet.artemis.core.exception.EntityNotFoundException; +import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; +import de.tum.cit.aet.artemis.iris.domain.session.IrisTextExerciseChatSession; + +/** + * Repository interface for managing {@link IrisTextExerciseChatSession} entities. + * Provides custom queries for finding text exercise chat sessions based on different criteria. + */ +public interface IrisTextExerciseChatSessionRepository extends ArtemisJpaRepository { + + /** + * Finds a list of {@link IrisTextExerciseChatSession} based on the exercise and user IDs. + * + * @param exerciseId The ID of the exercise. + * @param userId The ID of the user. + * @return A list of text exercise chat sessions sorted by creation date in descending order. + */ + @Query(""" + + SELECT s + FROM IrisTextExerciseChatSession s + WHERE s.exercise.id = :exerciseId + AND s.user.id = :userId + ORDER BY s.creationDate DESC + """) + List findByExerciseIdAndUserId(@Param("exerciseId") Long exerciseId, @Param("userId") Long userId); + + @Query(""" + SELECT s + FROM IrisTextExerciseChatSession s + WHERE s.exercise.id = :exerciseId + AND s.user.id = :userId + ORDER BY s.creationDate DESC + """) + List findSessionsByExerciseIdAndUserId(@Param("exerciseId") Long exerciseId, @Param("userId") Long userId, Pageable pageable); + + @EntityGraph(type = LOAD, attributePaths = "messages") + List findSessionsWithMessagesByIdIn(List ids); + + /** + * Finds the latest text exercise chat sessions by exercise ID and user ID, including their messages, with pagination support. + * This method avoids in-memory paging by retrieving the session IDs directly from the database. + * + * @param exerciseId the ID of the exercise to find the text exercise chat sessions for + * @param userId the ID of the user to find the text exercise chat sessions for + * @param pageable the pagination information + * @return a list of {@code IrisExerciseChatSession} with messages, or an empty list if no sessions are found + */ + default List findLatestByExerciseIdAndUserIdWithMessages(Long exerciseId, Long userId, Pageable pageable) { + List ids = findSessionsByExerciseIdAndUserId(exerciseId, userId, pageable).stream().map(DomainObject::getId).toList(); + + if (ids.isEmpty()) { + return Collections.emptyList(); + } + + return findSessionsWithMessagesByIdIn(ids); + } + + /** + * Finds a list of text exercise chat sessions or throws an exception if none are found. + * + * @param exerciseId The ID of the exercise. + * @param userId The ID of the user. + * @return A list of text exercise chat sessions. + * @throws EntityNotFoundException if no sessions are found. + */ + @NotNull + default List findByExerciseIdAndUserIdElseThrow(long exerciseId, long userId) throws EntityNotFoundException { + var result = findByExerciseIdAndUserId(exerciseId, userId); + if (result.isEmpty()) { + throw new EntityNotFoundException("Iris Text Exercise Chat Session"); + } + return result; + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisCompetencyGenerationService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisCompetencyGenerationService.java index 7c37831a611c..98182ae92b06 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisCompetencyGenerationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisCompetencyGenerationService.java @@ -17,7 +17,7 @@ import de.tum.cit.aet.artemis.iris.service.websocket.IrisWebsocketService; /** - * Service to handle the Competency generation subsytem of Iris. + * Service to handle the Competency generation subsystem of Iris. */ @Service @Profile(PROFILE_IRIS) diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisDefaultTemplateService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisDefaultTemplateService.java deleted file mode 100644 index 2a4757596bf1..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisDefaultTemplateService.java +++ /dev/null @@ -1,76 +0,0 @@ -package de.tum.cit.aet.artemis.iris.service; - -import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_IRIS; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.util.Optional; - -import org.apache.commons.io.IOUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.annotation.Profile; -import org.springframework.core.io.Resource; -import org.springframework.stereotype.Service; - -import de.tum.cit.aet.artemis.core.service.ResourceLoaderService; -import de.tum.cit.aet.artemis.iris.domain.IrisTemplate; - -/** - * Service that loads default Iris templates from the resources/templates/iris folder. - */ -@Profile(PROFILE_IRIS) -@Service -public class IrisDefaultTemplateService { - - private static final Logger log = LoggerFactory.getLogger(IrisDefaultTemplateService.class); - - private final ResourceLoaderService resourceLoaderService; - - public IrisDefaultTemplateService(ResourceLoaderService resourceLoaderService) { - this.resourceLoaderService = resourceLoaderService; - } - - /** - * Loads the default Iris template with the given file name. - * For example, "chat.hbs" will load the template from "resources/templates/iris/chat.hbs". - * - * @param templateFileName The file name of the template to load. - * @return The loaded Iris template, or an empty template if an IO error occurred. - */ - public IrisTemplate load(String templateFileName) { - Path filePath = Path.of("templates", "iris", templateFileName); - Resource resource = resourceLoaderService.getResource(filePath); - try { - String fileContent = IOUtils.toString(resource.getInputStream(), StandardCharsets.UTF_8); - return new IrisTemplate(fileContent); - } - catch (IOException e) { - log.error("Error while loading Iris template from file: {}", filePath, e); - return new IrisTemplate(""); - } - } - - /** - * Loads the global template version from the "resources/templates/iris/template-version.txt" file. - * - * @return an Optional containing the version loaded from the file, or an empty Optional if there was an error. - */ - public Optional loadGlobalTemplateVersion() { - Path filePath = Path.of("templates", "iris", "template-version.txt"); - Resource resource = resourceLoaderService.getResource(filePath); - try { - String fileContent = IOUtils.toString(resource.getInputStream(), StandardCharsets.UTF_8); - int version = Integer.parseInt(fileContent.trim()); - return Optional.of(version); - } - catch (IOException e) { - log.error("Error while loading global template version from file: {}", filePath, e); - } - catch (NumberFormatException e) { - log.error("Content of {} was not a parseable int!", filePath, e); - } - return Optional.empty(); - } -} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisSessionService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisSessionService.java index 41fa1247b739..308637e97bcf 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisSessionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisSessionService.java @@ -14,14 +14,14 @@ import de.tum.cit.aet.artemis.iris.domain.message.IrisMessage; import de.tum.cit.aet.artemis.iris.domain.session.IrisCourseChatSession; import de.tum.cit.aet.artemis.iris.domain.session.IrisExerciseChatSession; -import de.tum.cit.aet.artemis.iris.domain.session.IrisHestiaSession; import de.tum.cit.aet.artemis.iris.domain.session.IrisSession; +import de.tum.cit.aet.artemis.iris.domain.session.IrisTextExerciseChatSession; import de.tum.cit.aet.artemis.iris.service.session.IrisChatBasedFeatureInterface; import de.tum.cit.aet.artemis.iris.service.session.IrisCourseChatSessionService; import de.tum.cit.aet.artemis.iris.service.session.IrisExerciseChatSessionService; -import de.tum.cit.aet.artemis.iris.service.session.IrisHestiaSessionService; import de.tum.cit.aet.artemis.iris.service.session.IrisRateLimitedFeatureInterface; import de.tum.cit.aet.artemis.iris.service.session.IrisSubFeatureInterface; +import de.tum.cit.aet.artemis.iris.service.session.IrisTextExerciseChatSessionService; /** * Service for managing Iris sessions. @@ -32,18 +32,18 @@ public class IrisSessionService { private final UserRepository userRepository; + private final IrisTextExerciseChatSessionService irisTextExerciseChatSessionService; + private final IrisExerciseChatSessionService irisExerciseChatSessionService; private final IrisCourseChatSessionService irisCourseChatSessionService; - private final IrisHestiaSessionService irisHestiaSessionService; - - public IrisSessionService(UserRepository userRepository, IrisExerciseChatSessionService irisExerciseChatSessionService, - IrisCourseChatSessionService irisCourseChatSessionService, IrisHestiaSessionService irisHestiaSessionService) { + public IrisSessionService(UserRepository userRepository, IrisTextExerciseChatSessionService irisTextExerciseChatSessionService, + IrisExerciseChatSessionService irisExerciseChatSessionService, IrisCourseChatSessionService irisCourseChatSessionService) { this.userRepository = userRepository; + this.irisTextExerciseChatSessionService = irisTextExerciseChatSessionService; this.irisExerciseChatSessionService = irisExerciseChatSessionService; this.irisCourseChatSessionService = irisCourseChatSessionService; - this.irisHestiaSessionService = irisHestiaSessionService; } /** @@ -136,9 +136,9 @@ public void checkRateLimit(IrisSession session, User user) { @SuppressWarnings("unchecked") private IrisSubFeatureWrapper getIrisSessionSubService(S session) { return switch (session) { + case IrisTextExerciseChatSession chatSession -> (IrisSubFeatureWrapper) new IrisSubFeatureWrapper<>(irisTextExerciseChatSessionService, chatSession); case IrisExerciseChatSession chatSession -> (IrisSubFeatureWrapper) new IrisSubFeatureWrapper<>(irisExerciseChatSessionService, chatSession); case IrisCourseChatSession courseChatSession -> (IrisSubFeatureWrapper) new IrisSubFeatureWrapper<>(irisCourseChatSessionService, courseChatSession); - case IrisHestiaSession hestiaSession -> (IrisSubFeatureWrapper) new IrisSubFeatureWrapper<>(irisHestiaSessionService, hestiaSession); case null, default -> throw new BadRequestException("Unknown Iris session type " + session.getClass().getSimpleName()); }; } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisConnectorService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisConnectorService.java index 785fc59b9ed7..f41de6b6c97d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisConnectorService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisConnectorService.java @@ -19,10 +19,11 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import de.tum.cit.aet.artemis.iris.domain.settings.IrisSubSettingsType; import de.tum.cit.aet.artemis.iris.exception.IrisException; import de.tum.cit.aet.artemis.iris.exception.IrisForbiddenException; import de.tum.cit.aet.artemis.iris.exception.IrisInternalPyrisErrorException; -import de.tum.cit.aet.artemis.iris.service.pyris.dto.PyrisModelDTO; +import de.tum.cit.aet.artemis.iris.service.pyris.dto.PyrisVariantDTO; import de.tum.cit.aet.artemis.iris.service.pyris.dto.lectureingestionwebhook.PyrisWebhookLectureIngestionExecutionDTO; import de.tum.cit.aet.artemis.iris.web.open.PublicPyrisStatusUpdateResource; @@ -50,13 +51,14 @@ public PyrisConnectorService(@Qualifier("pyrisRestTemplate") RestTemplate restTe } /** - * Requests all available models from Pyris + * Requests all available variants from Pyris for a feature * - * @return A list of available Models as IrisModelDTO + * @param feature The feature to get the variants for + * @return A list of available Models as IrisVariantDTO */ - public List getOfferedModels() throws PyrisConnectorException { + public List getOfferedVariants(IrisSubSettingsType feature) throws PyrisConnectorException { try { - var response = restTemplate.getForEntity(pyrisUrl + "/api/v1/models", PyrisModelDTO[].class); + var response = restTemplate.getForEntity(pyrisUrl + "/api/v1/pipelines/" + feature.name() + "/variants", PyrisVariantDTO[].class); if (!response.getStatusCode().is2xxSuccessful() || !response.hasBody()) { throw new PyrisConnectorException("Could not fetch offered models"); } 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 aed62b6049c1..9403da9beb56 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 @@ -11,6 +11,7 @@ import de.tum.cit.aet.artemis.iris.service.IrisCompetencyGenerationService; 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.status.PyrisStageDTO; @@ -19,8 +20,10 @@ 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.TextExerciseChatJob; import de.tum.cit.aet.artemis.iris.service.session.IrisCourseChatSessionService; import de.tum.cit.aet.artemis.iris.service.session.IrisExerciseChatSessionService; +import de.tum.cit.aet.artemis.iris.service.session.IrisTextExerciseChatSessionService; @Service @Profile(PROFILE_IRIS) @@ -30,6 +33,8 @@ public class PyrisStatusUpdateService { private final IrisExerciseChatSessionService irisExerciseChatSessionService; + private final IrisTextExerciseChatSessionService irisTextExerciseChatSessionService; + private final IrisCourseChatSessionService courseChatSessionService; private final IrisCompetencyGenerationService competencyGenerationService; @@ -37,9 +42,11 @@ public class PyrisStatusUpdateService { private static final Logger log = LoggerFactory.getLogger(PyrisStatusUpdateService.class); public PyrisStatusUpdateService(PyrisJobService pyrisJobService, IrisExerciseChatSessionService irisExerciseChatSessionService, - IrisCourseChatSessionService courseChatSessionService, IrisCompetencyGenerationService competencyGenerationService) { + IrisTextExerciseChatSessionService irisTextExerciseChatSessionService, IrisCourseChatSessionService courseChatSessionService, + IrisCompetencyGenerationService competencyGenerationService) { this.pyrisJobService = pyrisJobService; this.irisExerciseChatSessionService = irisExerciseChatSessionService; + this.irisTextExerciseChatSessionService = irisTextExerciseChatSessionService; this.courseChatSessionService = courseChatSessionService; this.competencyGenerationService = competencyGenerationService; } @@ -56,6 +63,19 @@ public void handleStatusUpdate(ExerciseChatJob job, PyrisChatStatusUpdateDTO sta removeJobIfTerminated(statusUpdate.stages(), job.jobId()); } + /** + * Handles the status update of an exercise chat job and forwards it to + * {@link IrisTextExerciseChatSessionService#handleStatusUpdate(TextExerciseChatJob, PyrisTextExerciseChatStatusUpdateDTO)} + * + * @param job the job that is updated + * @param statusUpdate the status update + */ + public void handleStatusUpdate(TextExerciseChatJob job, PyrisTextExerciseChatStatusUpdateDTO statusUpdate) { + irisTextExerciseChatSessionService.handleStatusUpdate(job, statusUpdate); + + removeJobIfTerminated(statusUpdate.stages(), job.jobId()); + } + /** * Handles the status update of a course chat job and forwards it to * {@link de.tum.cit.aet.artemis.iris.service.session.IrisCourseChatSessionService#handleStatusUpdate(CourseChatJob, PyrisChatStatusUpdateDTO)} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/PyrisModelDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/PyrisVariantDTO.java similarity index 67% rename from src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/PyrisModelDTO.java rename to src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/PyrisVariantDTO.java index 705fada64870..ccfbecf7ee9a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/PyrisModelDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/PyrisVariantDTO.java @@ -3,5 +3,5 @@ import com.fasterxml.jackson.annotation.JsonInclude; @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record PyrisModelDTO(String id, String name, String description) { +public record PyrisVariantDTO(String id, String name, String description) { } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/chat/textexercise/PyrisTextExerciseChatPipelineExecutionDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/chat/textexercise/PyrisTextExerciseChatPipelineExecutionDTO.java new file mode 100644 index 000000000000..1b2cf59d9fdb --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/chat/textexercise/PyrisTextExerciseChatPipelineExecutionDTO.java @@ -0,0 +1,14 @@ +package de.tum.cit.aet.artemis.iris.service.pyris.dto.chat.textexercise; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.cit.aet.artemis.iris.service.pyris.dto.PyrisPipelineExecutionDTO; +import de.tum.cit.aet.artemis.iris.service.pyris.dto.data.PyrisMessageDTO; +import de.tum.cit.aet.artemis.iris.service.pyris.dto.data.PyrisTextExerciseDTO; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record PyrisTextExerciseChatPipelineExecutionDTO(PyrisPipelineExecutionDTO execution, PyrisTextExerciseDTO exercise, List conversation, + String currentSubmission) { +} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/chat/textexercise/PyrisTextExerciseChatStatusUpdateDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/chat/textexercise/PyrisTextExerciseChatStatusUpdateDTO.java new file mode 100644 index 000000000000..e8b5de7a950b --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/chat/textexercise/PyrisTextExerciseChatStatusUpdateDTO.java @@ -0,0 +1,11 @@ +package de.tum.cit.aet.artemis.iris.service.pyris.dto.chat.textexercise; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.cit.aet.artemis.iris.service.pyris.dto.status.PyrisStageDTO; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record PyrisTextExerciseChatStatusUpdateDTO(String result, List stages) { +} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/data/PyrisTextExerciseDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/data/PyrisTextExerciseDTO.java new file mode 100644 index 000000000000..8ba3b1f9aaa8 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/data/PyrisTextExerciseDTO.java @@ -0,0 +1,33 @@ +package de.tum.cit.aet.artemis.iris.service.pyris.dto.data; + +import java.time.Instant; +import java.time.chrono.ChronoZonedDateTime; +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.cit.aet.artemis.text.domain.TextExercise; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record PyrisTextExerciseDTO(long id, String title, PyrisCourseDTO course, String problemStatement, Instant startDate, Instant endDate) { + + /** + * Create a new PyrisTextExerciseDTO from the given TextExercise + * + * @param exercise the exercise + * @return the dto + */ + public static PyrisTextExerciseDTO of(TextExercise exercise) { + // @formatter:off + return new PyrisTextExerciseDTO( + exercise.getId(), + exercise.getTitle(), + new PyrisCourseDTO(exercise.getCourseViaExerciseGroupOrCourseMember()), + exercise.getProblemStatement(), + Optional.ofNullable(exercise.getStartDate()).map(ChronoZonedDateTime::toInstant).orElse(null), + Optional.ofNullable(exercise.getDueDate()).map(ChronoZonedDateTime::toInstant).orElse(null) + ); + // @formatter:on + } + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/job/TextExerciseChatJob.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/job/TextExerciseChatJob.java new file mode 100644 index 000000000000..b7dd4bb50046 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/job/TextExerciseChatJob.java @@ -0,0 +1,20 @@ +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; +import de.tum.cit.aet.artemis.exercise.domain.Exercise; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record TextExerciseChatJob(String jobId, long courseId, long exerciseId, long sessionId) implements PyrisJob { + + @Override + public boolean canAccess(Course course) { + return course.getId().equals(courseId); + } + + @Override + public boolean canAccess(Exercise exercise) { + return exercise.getId().equals(exerciseId); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisCourseChatSessionService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisCourseChatSessionService.java index a2c404b13103..6dea7a728ca6 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisCourseChatSessionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisCourseChatSessionService.java @@ -116,7 +116,8 @@ public void checkRateLimit(User user) { */ @Override public void requestAndHandleResponse(IrisCourseChatSession session) { - requestAndHandleResponse(session, "default", null); + var variant = irisSettingsService.getCombinedIrisSettingsFor(session.getCourse(), false).irisChatSettings().selectedVariant(); + requestAndHandleResponse(session, variant, null); } private void requestAndHandleResponse(IrisCourseChatSession session, String variant, CompetencyJol competencyJol) { diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisExerciseChatSessionService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisExerciseChatSessionService.java index cec0a9322134..d520540a2db4 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisExerciseChatSessionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisExerciseChatSessionService.java @@ -142,13 +142,11 @@ public void requestAndHandleResponse(IrisExerciseChatSession session) { if (chatSession.getExercise().isExamExercise()) { throw new ConflictException("Iris is not supported for exam exercises", "Iris", "irisExamExercise"); } - // TODO support more exercise types var exercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationElseThrow(chatSession.getExercise().getId()); var latestSubmission = getLatestSubmissionIfExists(exercise, chatSession.getUser()); - // TODO: Use settings to determine the variant - // var irisSettings = irisSettingsService.getCombinedIrisSettingsFor(chatSession.getExercise(), false); - pyrisPipelineService.executeExerciseChatPipeline("default", latestSubmission, exercise, chatSession); + var variant = irisSettingsService.getCombinedIrisSettingsFor(session.getExercise(), false).irisChatSettings().selectedVariant(); + pyrisPipelineService.executeExerciseChatPipeline(variant, latestSubmission, exercise, chatSession); } private Optional getLatestSubmissionIfExists(ProgrammingExercise exercise, User user) { diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisHestiaSessionService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisHestiaSessionService.java deleted file mode 100644 index 6762b6d23d43..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisHestiaSessionService.java +++ /dev/null @@ -1,113 +0,0 @@ -package de.tum.cit.aet.artemis.iris.service.session; - -import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_IRIS; - -import java.time.ZonedDateTime; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Service; - -import com.fasterxml.jackson.annotation.JsonInclude; - -import de.tum.cit.aet.artemis.core.domain.User; -import de.tum.cit.aet.artemis.core.security.Role; -import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; -import de.tum.cit.aet.artemis.iris.domain.session.IrisHestiaSession; -import de.tum.cit.aet.artemis.iris.domain.settings.IrisSubSettingsType; -import de.tum.cit.aet.artemis.iris.repository.IrisHestiaSessionRepository; -import de.tum.cit.aet.artemis.iris.repository.IrisSessionRepository; -import de.tum.cit.aet.artemis.iris.service.pyris.PyrisConnectorService; -import de.tum.cit.aet.artemis.iris.service.settings.IrisSettingsService; -import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; -import de.tum.cit.aet.artemis.programming.domain.hestia.CodeHint; - -/** - * Service to handle the Hestia integration of Iris. - */ -@Service -@Profile(PROFILE_IRIS) -public class IrisHestiaSessionService implements IrisButtonBasedFeatureInterface { - - private static final Logger log = LoggerFactory.getLogger(IrisHestiaSessionService.class); - - private final IrisSettingsService irisSettingsService; - - private final AuthorizationCheckService authCheckService; - - private final IrisSessionRepository irisSessionRepository; - - private final IrisHestiaSessionRepository irisHestiaSessionRepository; - - public IrisHestiaSessionService(PyrisConnectorService pyrisConnectorService, IrisSettingsService irisSettingsService, AuthorizationCheckService authCheckService, - IrisSessionRepository irisSessionRepository, IrisHestiaSessionRepository irisHestiaSessionRepository) { - this.irisSettingsService = irisSettingsService; - this.authCheckService = authCheckService; - this.irisSessionRepository = irisSessionRepository; - this.irisHestiaSessionRepository = irisHestiaSessionRepository; - } - - /** - * Creates a new Iris session for the given code hint. - * If there is already an existing session for the code hint from the last hour, it will be returned instead. - * - * @param codeHint The code hint to create the session for - * @return The Iris session for the code hint - */ - public IrisHestiaSession getOrCreateSession(CodeHint codeHint) { - var existingSessions = irisHestiaSessionRepository.findByCodeHintIdOrderByCreationDateDesc(codeHint.getId()); - // Return the newest session if there is one and it is not older than 1 hour - if (!existingSessions.isEmpty() && existingSessions.getFirst().getCreationDate().plusHours(1).isAfter(ZonedDateTime.now())) { - checkHasAccessTo(null, existingSessions.getFirst()); - return existingSessions.getFirst(); - } - - // Otherwise create a new session - var irisSession = new IrisHestiaSession(); - irisSession.setCodeHint(codeHint); - checkHasAccessTo(null, irisSession); - irisSession = irisSessionRepository.save(irisSession); - return irisSession; - } - - @JsonInclude(JsonInclude.Include.NON_EMPTY) - record HestiaDTO(CodeHint codeHint, IrisHestiaSession session, ProgrammingExercise exercise) { - } - - /** - * Generates the description and content for a code hint. - * It does not directly save the code hint, but instead returns it with the generated description and content. - * This way the instructor can still modify the code hint before saving it or discard the changes. - * - * @param session The Iris session to generate the description for - * @return The code hint with the generated description and content - */ - @Override - public CodeHint executeRequest(IrisHestiaSession session) { - // TODO: Re-add in a future PR. Remember to reenable the test cases! - return null; - } - - /** - * Checks if the user has at least the given role for the exercise of the code hint. - * - * @param user The user to check the access for - * @param session The Iris session to check the access for - */ - @Override - public void checkHasAccessTo(User user, IrisHestiaSession session) { - var exercise = session.getCodeHint().getExercise(); - authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.EDITOR, exercise, user); - } - - /** - * Not supported for Iris Hestia sessions. - * - * @param session The session to get a message for - */ - @Override - public void checkIsFeatureActivatedFor(IrisHestiaSession session) { - irisSettingsService.isEnabledForElseThrow(IrisSubSettingsType.HESTIA, session.getCodeHint().getExercise()); - } -} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisTextExerciseChatSessionService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisTextExerciseChatSessionService.java new file mode 100644 index 000000000000..4520417aad48 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisTextExerciseChatSessionService.java @@ -0,0 +1,170 @@ +package de.tum.cit.aet.artemis.iris.service.session; + +import java.util.Comparator; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException; +import de.tum.cit.aet.artemis.core.exception.ConflictException; +import de.tum.cit.aet.artemis.core.security.Role; +import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; +import de.tum.cit.aet.artemis.exercise.domain.Submission; +import de.tum.cit.aet.artemis.exercise.repository.StudentParticipationRepository; +import de.tum.cit.aet.artemis.iris.domain.message.IrisMessage; +import de.tum.cit.aet.artemis.iris.domain.message.IrisMessageSender; +import de.tum.cit.aet.artemis.iris.domain.message.IrisTextMessageContent; +import de.tum.cit.aet.artemis.iris.domain.session.IrisTextExerciseChatSession; +import de.tum.cit.aet.artemis.iris.domain.settings.IrisSubSettingsType; +import de.tum.cit.aet.artemis.iris.repository.IrisSessionRepository; +import de.tum.cit.aet.artemis.iris.service.IrisMessageService; +import de.tum.cit.aet.artemis.iris.service.IrisRateLimitService; +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.chat.textexercise.PyrisTextExerciseChatPipelineExecutionDTO; +import de.tum.cit.aet.artemis.iris.service.pyris.dto.chat.textexercise.PyrisTextExerciseChatStatusUpdateDTO; +import de.tum.cit.aet.artemis.iris.service.pyris.dto.data.PyrisMessageDTO; +import de.tum.cit.aet.artemis.iris.service.pyris.dto.data.PyrisTextExerciseDTO; +import de.tum.cit.aet.artemis.iris.service.pyris.job.TextExerciseChatJob; +import de.tum.cit.aet.artemis.iris.service.settings.IrisSettingsService; +import de.tum.cit.aet.artemis.iris.service.websocket.IrisChatWebsocketService; +import de.tum.cit.aet.artemis.text.domain.TextSubmission; +import de.tum.cit.aet.artemis.text.repository.TextExerciseRepository; + +@Service +@Profile("iris") +public class IrisTextExerciseChatSessionService implements IrisChatBasedFeatureInterface, IrisRateLimitedFeatureInterface { + + private final IrisSettingsService irisSettingsService; + + private final IrisSessionRepository irisSessionRepository; + + private final IrisRateLimitService rateLimitService; + + private final IrisMessageService irisMessageService; + + private final TextExerciseRepository textExerciseRepository; + + private final StudentParticipationRepository studentParticipationRepository; + + private final PyrisPipelineService pyrisPipelineService; + + private final PyrisJobService pyrisJobService; + + private final IrisChatWebsocketService irisChatWebsocketService; + + private final AuthorizationCheckService authCheckService; + + public IrisTextExerciseChatSessionService(IrisSettingsService irisSettingsService, IrisSessionRepository irisSessionRepository, IrisRateLimitService rateLimitService, + IrisMessageService irisMessageService, TextExerciseRepository textExerciseRepository, StudentParticipationRepository studentParticipationRepository, + PyrisPipelineService pyrisPipelineService, PyrisJobService pyrisJobService, IrisChatWebsocketService irisChatWebsocketService, + AuthorizationCheckService authCheckService) { + this.irisSettingsService = irisSettingsService; + this.irisSessionRepository = irisSessionRepository; + this.rateLimitService = rateLimitService; + this.irisMessageService = irisMessageService; + this.textExerciseRepository = textExerciseRepository; + this.studentParticipationRepository = studentParticipationRepository; + this.pyrisPipelineService = pyrisPipelineService; + this.pyrisJobService = pyrisJobService; + this.irisChatWebsocketService = irisChatWebsocketService; + this.authCheckService = authCheckService; + } + + @Override + public void sendOverWebsocket(IrisTextExerciseChatSession session, IrisMessage message) { + irisChatWebsocketService.sendMessage(session, message, null); + } + + @Override + public void requestAndHandleResponse(IrisTextExerciseChatSession irisSession) { + var session = (IrisTextExerciseChatSession) irisSessionRepository.findByIdWithMessagesAndContents(irisSession.getId()); + if (session.getExercise().isExamExercise()) { + throw new ConflictException("Iris is not supported for exam exercises", "Iris", "irisExamExercise"); + } + var exercise = textExerciseRepository.findByIdElseThrow(session.getExercise().getId()); + if (!irisSettingsService.isEnabledFor(IrisSubSettingsType.TEXT_EXERCISE_CHAT, exercise)) { + throw new ConflictException("Iris is not enabled for this exercise", "Iris", "irisDisabled"); + } + var course = exercise.getCourseViaExerciseGroupOrCourseMember(); + // TODO: Once we can receive client form data through the IrisMessageResource, we should use that instead of fetching the latest submission to get the text + var participation = studentParticipationRepository.findWithEagerLegalSubmissionsByExerciseIdAndStudentLogin(exercise.getId(), session.getUser().getLogin()); + var latestSubmission = participation.flatMap(p -> p.getSubmissions().stream().max(Comparator.comparingLong(Submission::getId))).orElse(null); + String latestSubmissionText; + if (latestSubmission instanceof TextSubmission textSubmission) { + latestSubmissionText = textSubmission.getText(); + } + else { + latestSubmissionText = null; + } + var conversation = session.getMessages().stream().map(PyrisMessageDTO::of).toList(); + // @formatter:off + pyrisPipelineService.executePipeline( + "text-exercise-chat", + "default", + pyrisJobService.createTokenForJob(token -> new TextExerciseChatJob(token, course.getId(), exercise.getId(), session.getId())), + dto -> new PyrisTextExerciseChatPipelineExecutionDTO(dto, PyrisTextExerciseDTO.of(exercise), conversation, latestSubmissionText), + stages -> irisChatWebsocketService.sendMessage(session, null, stages) + ); + // @formatter:on + } + + /** + * Handles the status update of a text exercise chat job. + * + * @param job The job that is updated + * @param statusUpdate The status update + */ + public void handleStatusUpdate(TextExerciseChatJob job, PyrisTextExerciseChatStatusUpdateDTO statusUpdate) { + var session = (IrisTextExerciseChatSession) irisSessionRepository.findByIdElseThrow(job.sessionId()); + if (statusUpdate.result() != null) { + var message = session.newMessage(); + message.addContent(new IrisTextMessageContent(statusUpdate.result())); + IrisMessage savedMessage = irisMessageService.saveMessage(message, session, IrisMessageSender.LLM); + irisChatWebsocketService.sendMessage(session, savedMessage, statusUpdate.stages()); + } + else { + irisChatWebsocketService.sendMessage(session, null, statusUpdate.stages()); + } + } + + @Override + public void checkHasAccessTo(User user, IrisTextExerciseChatSession session) { + // TODO: This check is probably unnecessary since we are fetching the sessions from the database with the user ID already + if (!session.getUser().equals(user)) { + throw new AccessForbiddenException("Iris Text Exercise Chat Session", session.getId()); + } + // TODO: This check is probably unnecessary as the endpoint already checks it via the @EnforceAtLeastStudentInExercise annotation + authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.STUDENT, session.getExercise(), user); + } + + /** + * This method returns true if the user has access to the given session. + * The user has access iff the user is a student in the exercise's course, + * and they are the same user that created the session. + * + * @param user The user to check + * @param session The session to check + * @return True if the user has access, false otherwise + */ + public boolean hasAccess(User user, IrisTextExerciseChatSession session) { + try { + checkHasAccessTo(user, session); + return true; + } + catch (AccessForbiddenException e) { + return false; + } + } + + @Override + public void checkIsFeatureActivatedFor(IrisTextExerciseChatSession session) { + irisSettingsService.isEnabledForElseThrow(IrisSubSettingsType.TEXT_EXERCISE_CHAT, session.getExercise()); + } + + @Override + public void checkRateLimit(User user) { + rateLimitService.checkRateLimitElseThrow(user); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSettingsService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSettingsService.java index 39bb14ff31cd..fb8820a4c08b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSettingsService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSettingsService.java @@ -9,7 +9,8 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.Objects; -import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; import java.util.function.Supplier; import org.springframework.boot.context.event.ApplicationReadyEvent; @@ -24,20 +25,18 @@ import de.tum.cit.aet.artemis.core.exception.ConflictException; import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; import de.tum.cit.aet.artemis.exercise.domain.Exercise; -import de.tum.cit.aet.artemis.iris.domain.IrisTemplate; import de.tum.cit.aet.artemis.iris.domain.settings.IrisChatSubSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisCompetencyGenerationSubSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisCourseSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisExerciseSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisGlobalSettings; -import de.tum.cit.aet.artemis.iris.domain.settings.IrisHestiaSubSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisLectureIngestionSubSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisSubSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisSubSettingsType; +import de.tum.cit.aet.artemis.iris.domain.settings.IrisTextExerciseChatSubSettings; import de.tum.cit.aet.artemis.iris.dto.IrisCombinedSettingsDTO; import de.tum.cit.aet.artemis.iris.repository.IrisSettingsRepository; -import de.tum.cit.aet.artemis.iris.service.IrisDefaultTemplateService; /** * Service for managing {@link IrisSettings}. @@ -54,34 +53,14 @@ public class IrisSettingsService { private final IrisSubSettingsService irisSubSettingsService; - private final IrisDefaultTemplateService irisDefaultTemplateService; - private final AuthorizationCheckService authCheckService; - public IrisSettingsService(IrisSettingsRepository irisSettingsRepository, IrisSubSettingsService irisSubSettingsService, IrisDefaultTemplateService irisDefaultTemplateService, - AuthorizationCheckService authCheckService) { + public IrisSettingsService(IrisSettingsRepository irisSettingsRepository, IrisSubSettingsService irisSubSettingsService, AuthorizationCheckService authCheckService) { this.irisSettingsRepository = irisSettingsRepository; this.irisSubSettingsService = irisSubSettingsService; - this.irisDefaultTemplateService = irisDefaultTemplateService; this.authCheckService = authCheckService; } - private Optional loadGlobalTemplateVersion() { - return irisDefaultTemplateService.loadGlobalTemplateVersion(); - } - - private IrisTemplate loadDefaultChatTemplate() { - return irisDefaultTemplateService.load("chat.hbs"); - } - - private IrisTemplate loadDefaultHestiaTemplate() { - return irisDefaultTemplateService.load("hestia.hbs"); - } - - private IrisTemplate loadDefaultCompetencyGenerationTemplate() { - return irisDefaultTemplateService.load("competency-generation.hbs"); - } - /** * Hooks into the {@link ApplicationReadyEvent} and creates or updates the global IrisSettings object on startup. * @@ -98,10 +77,6 @@ public void execute(ApplicationReadyEvent event) throws Exception { if (allGlobalSettings.size() > 1) { var maxIdSettings = allGlobalSettings.stream().max(Comparator.comparingLong(IrisSettings::getId)).orElseThrow(); allGlobalSettings.stream().filter(settings -> !Objects.equals(settings.getId(), maxIdSettings.getId())).forEach(irisSettingsRepository::delete); - autoUpdateGlobalSettings(maxIdSettings); - } - else { - autoUpdateGlobalSettings(allGlobalSettings.stream().findFirst().get()); } } @@ -110,46 +85,21 @@ public void execute(ApplicationReadyEvent event) throws Exception { */ private void createInitialGlobalSettings() { var settings = new IrisGlobalSettings(); - settings.setCurrentVersion(loadGlobalTemplateVersion().orElse(0)); initializeIrisChatSettings(settings); + initializeIrisTextExerciseChatSettings(settings); initializeIrisLectureIngestionSettings(settings); - initializeIrisHestiaSettings(settings); initializeIrisCompetencyGenerationSettings(settings); irisSettingsRepository.save(settings); } - /** - * Auto updates the global IrisSettings object if the current version is outdated. - * - * @param settings The global IrisSettings object to update - */ - private void autoUpdateGlobalSettings(IrisGlobalSettings settings) { - Optional globalVersion = loadGlobalTemplateVersion(); - if (globalVersion.isEmpty() || settings.getCurrentVersion() < globalVersion.get()) { - if (settings.isEnableAutoUpdateChat() || settings.getIrisChatSettings() == null) { - initializeIrisChatSettings(settings); - } - if (settings.isEnableAutoUpdateLectureIngestion() || settings.getIrisLectureIngestionSettings() == null) { - initializeIrisLectureIngestionSettings(settings); - } - if (settings.isEnableAutoUpdateHestia() || settings.getIrisHestiaSettings() == null) { - initializeIrisHestiaSettings(settings); - } - if (settings.isEnableAutoUpdateCompetencyGeneration() || settings.getIrisCompetencyGenerationSettings() == null) { - initializeIrisCompetencyGenerationSettings(settings); - } - - globalVersion.ifPresent(settings::setCurrentVersion); - saveIrisSettings(settings); - } - } - private static T initializeSettings(T settings, Supplier constructor) { if (settings == null) { settings = constructor.get(); settings.setEnabled(false); + settings.setAllowedVariants(new TreeSet<>(Set.of("default"))); + settings.setSelectedVariant("default"); } return settings; } @@ -157,27 +107,24 @@ private static T initializeSettings(T settings, Supp private void initializeIrisChatSettings(IrisGlobalSettings settings) { var irisChatSettings = settings.getIrisChatSettings(); irisChatSettings = initializeSettings(irisChatSettings, IrisChatSubSettings::new); - irisChatSettings.setTemplate(loadDefaultChatTemplate()); settings.setIrisChatSettings(irisChatSettings); } + private void initializeIrisTextExerciseChatSettings(IrisGlobalSettings settings) { + var irisChatSettings = settings.getIrisTextExerciseChatSettings(); + irisChatSettings = initializeSettings(irisChatSettings, IrisTextExerciseChatSubSettings::new); + settings.setIrisTextExerciseChatSettings(irisChatSettings); + } + private void initializeIrisLectureIngestionSettings(IrisGlobalSettings settings) { var irisLectureIngestionSettings = settings.getIrisLectureIngestionSettings(); irisLectureIngestionSettings = initializeSettings(irisLectureIngestionSettings, IrisLectureIngestionSubSettings::new); settings.setIrisLectureIngestionSettings(irisLectureIngestionSettings); } - private void initializeIrisHestiaSettings(IrisGlobalSettings settings) { - var irisHestiaSettings = settings.getIrisHestiaSettings(); - irisHestiaSettings = initializeSettings(irisHestiaSettings, IrisHestiaSubSettings::new); - irisHestiaSettings.setTemplate(loadDefaultHestiaTemplate()); - settings.setIrisHestiaSettings(irisHestiaSettings); - } - private void initializeIrisCompetencyGenerationSettings(IrisGlobalSettings settings) { var irisCompetencyGenerationSettings = settings.getIrisCompetencyGenerationSettings(); irisCompetencyGenerationSettings = initializeSettings(irisCompetencyGenerationSettings, IrisCompetencyGenerationSubSettings::new); - irisCompetencyGenerationSettings.setTemplate(loadDefaultCompetencyGenerationTemplate()); settings.setIrisCompetencyGenerationSettings(irisCompetencyGenerationSettings); } @@ -214,9 +161,6 @@ private T saveNewIrisSettings(T settings) { if (settings instanceof IrisGlobalSettings) { throw new BadRequestAlertException("You can not create new global settings", "IrisSettings", "notGlobal"); } - if (!settings.isValid()) { - throw new BadRequestAlertException("New Iris settings are not valid", "IrisSettings", "notValid"); - } if (settings instanceof IrisCourseSettings courseSettings && irisSettingsRepository.findCourseSettings(courseSettings.getCourse().getId()).isPresent()) { throw new ConflictException("Iris settings for this course already exist", "IrisSettings", "alreadyExists"); } @@ -241,9 +185,6 @@ private T updateIrisSettings(long existingSettingsId, T if (!Objects.equals(existingSettingsId, settingsUpdate.getId())) { throw new ConflictException("Existing Iris settings ID does not match update ID", "IrisSettings", "idMismatch"); } - if (!settingsUpdate.isValid()) { - throw new BadRequestAlertException("Updated Iris settings are not valid", "IrisSettings", "notValid"); - } var existingSettings = irisSettingsRepository.findByIdElseThrow(existingSettingsId); @@ -269,19 +210,32 @@ else if (existingSettings instanceof IrisExerciseSettings exerciseSettings && se * @return The updated global Iris settings */ private IrisGlobalSettings updateGlobalSettings(IrisGlobalSettings existingSettings, IrisGlobalSettings settingsUpdate) { - existingSettings.setCurrentVersion(settingsUpdate.getCurrentVersion()); - - existingSettings.setEnableAutoUpdateChat(settingsUpdate.isEnableAutoUpdateChat()); - existingSettings.setEnableAutoUpdateLectureIngestion(settingsUpdate.isEnableAutoUpdateLectureIngestion()); - existingSettings.setEnableAutoUpdateHestia(settingsUpdate.isEnableAutoUpdateHestia()); - existingSettings.setEnableAutoUpdateCompetencyGeneration(settingsUpdate.isEnableAutoUpdateCompetencyGeneration()); - - existingSettings.setIrisLectureIngestionSettings( - irisSubSettingsService.update(existingSettings.getIrisLectureIngestionSettings(), settingsUpdate.getIrisLectureIngestionSettings(), null, GLOBAL)); - existingSettings.setIrisChatSettings(irisSubSettingsService.update(existingSettings.getIrisChatSettings(), settingsUpdate.getIrisChatSettings(), null, GLOBAL)); - existingSettings.setIrisHestiaSettings(irisSubSettingsService.update(existingSettings.getIrisHestiaSettings(), settingsUpdate.getIrisHestiaSettings(), null, GLOBAL)); - existingSettings.setIrisCompetencyGenerationSettings( - irisSubSettingsService.update(existingSettings.getIrisCompetencyGenerationSettings(), settingsUpdate.getIrisCompetencyGenerationSettings(), null, GLOBAL)); + // @formatter:off + existingSettings.setIrisLectureIngestionSettings(irisSubSettingsService.update( + existingSettings.getIrisLectureIngestionSettings(), + settingsUpdate.getIrisLectureIngestionSettings(), + null, + GLOBAL + )); + existingSettings.setIrisTextExerciseChatSettings(irisSubSettingsService.update( + existingSettings.getIrisTextExerciseChatSettings(), + settingsUpdate.getIrisTextExerciseChatSettings(), + null, + GLOBAL + )); + existingSettings.setIrisChatSettings(irisSubSettingsService.update( + existingSettings.getIrisChatSettings(), + settingsUpdate.getIrisChatSettings(), + null, + GLOBAL + )); + existingSettings.setIrisCompetencyGenerationSettings(irisSubSettingsService.update( + existingSettings.getIrisCompetencyGenerationSettings(), + settingsUpdate.getIrisCompetencyGenerationSettings(), + null, + GLOBAL + )); + // @formatter:on return irisSettingsRepository.save(existingSettings); } @@ -295,14 +249,32 @@ private IrisGlobalSettings updateGlobalSettings(IrisGlobalSettings existingSetti */ private IrisCourseSettings updateCourseSettings(IrisCourseSettings existingSettings, IrisCourseSettings settingsUpdate) { var parentSettings = getCombinedIrisGlobalSettings(); - existingSettings.setIrisChatSettings( - irisSubSettingsService.update(existingSettings.getIrisChatSettings(), settingsUpdate.getIrisChatSettings(), parentSettings.irisChatSettings(), COURSE)); - existingSettings.setIrisLectureIngestionSettings(irisSubSettingsService.update(existingSettings.getIrisLectureIngestionSettings(), - settingsUpdate.getIrisLectureIngestionSettings(), parentSettings.irisLectureIngestionSettings(), COURSE)); - existingSettings.setIrisHestiaSettings( - irisSubSettingsService.update(existingSettings.getIrisHestiaSettings(), settingsUpdate.getIrisHestiaSettings(), parentSettings.irisHestiaSettings(), COURSE)); - existingSettings.setIrisCompetencyGenerationSettings(irisSubSettingsService.update(existingSettings.getIrisCompetencyGenerationSettings(), - settingsUpdate.getIrisCompetencyGenerationSettings(), parentSettings.irisCompetencyGenerationSettings(), COURSE)); + // @formatter:off + existingSettings.setIrisChatSettings(irisSubSettingsService.update( + existingSettings.getIrisChatSettings(), + settingsUpdate.getIrisChatSettings(), + parentSettings.irisChatSettings(), + COURSE + )); + existingSettings.setIrisTextExerciseChatSettings(irisSubSettingsService.update( + existingSettings.getIrisTextExerciseChatSettings(), + settingsUpdate.getIrisTextExerciseChatSettings(), + parentSettings.irisTextExerciseChatSettings(), + COURSE + )); + existingSettings.setIrisLectureIngestionSettings(irisSubSettingsService.update( + existingSettings.getIrisLectureIngestionSettings(), + settingsUpdate.getIrisLectureIngestionSettings(), + parentSettings.irisLectureIngestionSettings(), + COURSE + )); + existingSettings.setIrisCompetencyGenerationSettings(irisSubSettingsService.update( + existingSettings.getIrisCompetencyGenerationSettings(), + settingsUpdate.getIrisCompetencyGenerationSettings(), + parentSettings.irisCompetencyGenerationSettings(), + COURSE + )); + // @formatter:on return irisSettingsRepository.save(existingSettings); } @@ -316,8 +288,20 @@ private IrisCourseSettings updateCourseSettings(IrisCourseSettings existingSetti */ private IrisExerciseSettings updateExerciseSettings(IrisExerciseSettings existingSettings, IrisExerciseSettings settingsUpdate) { var parentSettings = getCombinedIrisSettingsFor(existingSettings.getExercise().getCourseViaExerciseGroupOrCourseMember(), false); - existingSettings.setIrisChatSettings( - irisSubSettingsService.update(existingSettings.getIrisChatSettings(), settingsUpdate.getIrisChatSettings(), parentSettings.irisChatSettings(), EXERCISE)); + // @formatter:off + existingSettings.setIrisChatSettings(irisSubSettingsService.update( + existingSettings.getIrisChatSettings(), + settingsUpdate.getIrisChatSettings(), + parentSettings.irisChatSettings(), + EXERCISE + )); + existingSettings.setIrisTextExerciseChatSettings(irisSubSettingsService.update( + existingSettings.getIrisTextExerciseChatSettings(), + settingsUpdate.getIrisTextExerciseChatSettings(), + parentSettings.irisTextExerciseChatSettings(), + EXERCISE + )); + // @formatter:on return irisSettingsRepository.save(existingSettings); } @@ -381,9 +365,14 @@ public IrisCombinedSettingsDTO getCombinedIrisGlobalSettings() { var settingsList = new ArrayList(); settingsList.add(getGlobalSettings()); - return new IrisCombinedSettingsDTO(irisSubSettingsService.combineChatSettings(settingsList, false), - irisSubSettingsService.combineLectureIngestionSubSettings(settingsList, false), irisSubSettingsService.combineHestiaSettings(settingsList, false), - irisSubSettingsService.combineCompetencyGenerationSettings(settingsList, false)); + // @formatter:off + return new IrisCombinedSettingsDTO( + irisSubSettingsService.combineChatSettings(settingsList, false), + irisSubSettingsService.combineTextExerciseChatSettings(settingsList, false), + irisSubSettingsService.combineLectureIngestionSubSettings(settingsList, false), + irisSubSettingsService.combineCompetencyGenerationSettings(settingsList, false) + ); + // @formatter:on } /** @@ -401,9 +390,14 @@ public IrisCombinedSettingsDTO getCombinedIrisSettingsFor(Course course, boolean settingsList.add(getGlobalSettings()); settingsList.add(irisSettingsRepository.findCourseSettings(course.getId()).orElse(null)); - return new IrisCombinedSettingsDTO(irisSubSettingsService.combineChatSettings(settingsList, minimal), - irisSubSettingsService.combineLectureIngestionSubSettings(settingsList, minimal), irisSubSettingsService.combineHestiaSettings(settingsList, minimal), - irisSubSettingsService.combineCompetencyGenerationSettings(settingsList, minimal)); + // @formatter:off + return new IrisCombinedSettingsDTO( + irisSubSettingsService.combineChatSettings(settingsList, minimal), + irisSubSettingsService.combineTextExerciseChatSettings(settingsList, minimal), + irisSubSettingsService.combineLectureIngestionSubSettings(settingsList, minimal), + irisSubSettingsService.combineCompetencyGenerationSettings(settingsList, minimal) + ); + // @formatter:on } /** @@ -422,9 +416,14 @@ public IrisCombinedSettingsDTO getCombinedIrisSettingsFor(Exercise exercise, boo settingsList.add(getRawIrisSettingsFor(exercise.getCourseViaExerciseGroupOrCourseMember())); settingsList.add(getRawIrisSettingsFor(exercise)); - return new IrisCombinedSettingsDTO(irisSubSettingsService.combineChatSettings(settingsList, minimal), - irisSubSettingsService.combineLectureIngestionSubSettings(settingsList, minimal), irisSubSettingsService.combineHestiaSettings(settingsList, minimal), - irisSubSettingsService.combineCompetencyGenerationSettings(settingsList, minimal)); + // @formatter:off + return new IrisCombinedSettingsDTO( + irisSubSettingsService.combineChatSettings(settingsList, minimal), + irisSubSettingsService.combineTextExerciseChatSettings(settingsList, minimal), + irisSubSettingsService.combineLectureIngestionSubSettings(settingsList, minimal), + irisSubSettingsService.combineCompetencyGenerationSettings(settingsList, minimal) + ); + // @formatter:on } /** @@ -450,8 +449,8 @@ public IrisCourseSettings getDefaultSettingsFor(Course course) { settings.setCourse(course); settings.setIrisLectureIngestionSettings(new IrisLectureIngestionSubSettings()); settings.setIrisChatSettings(new IrisChatSubSettings()); - settings.setIrisHestiaSettings(new IrisHestiaSubSettings()); settings.setIrisCompetencyGenerationSettings(new IrisCompetencyGenerationSubSettings()); + settings.setIrisTextExerciseChatSettings(new IrisTextExerciseChatSubSettings()); return settings; } @@ -466,6 +465,7 @@ public IrisExerciseSettings getDefaultSettingsFor(Exercise exercise) { var settings = new IrisExerciseSettings(); settings.setExercise(exercise); settings.setIrisChatSettings(new IrisChatSubSettings()); + settings.setIrisTextExerciseChatSettings(new IrisTextExerciseChatSubSettings()); return settings; } @@ -523,7 +523,7 @@ public void deleteSettingsFor(Exercise exercise) { private boolean isFeatureEnabledInSettings(IrisCombinedSettingsDTO settings, IrisSubSettingsType type) { return switch (type) { case CHAT -> settings.irisChatSettings().enabled(); - case HESTIA -> settings.irisHestiaSettings().enabled(); + case TEXT_EXERCISE_CHAT -> settings.irisTextExerciseChatSettings().enabled(); case COMPETENCY_GENERATION -> settings.irisCompetencyGenerationSettings().enabled(); case LECTURE_INGESTION -> settings.irisLectureIngestionSettings().enabled(); }; diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSubSettingsService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSubSettingsService.java index dfd555bb59c5..4e804701151a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSubSettingsService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSubSettingsService.java @@ -15,19 +15,18 @@ import org.springframework.stereotype.Service; import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; -import de.tum.cit.aet.artemis.iris.domain.IrisTemplate; import de.tum.cit.aet.artemis.iris.domain.settings.IrisChatSubSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisCompetencyGenerationSubSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisExerciseSettings; -import de.tum.cit.aet.artemis.iris.domain.settings.IrisHestiaSubSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisLectureIngestionSubSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisSettingsType; import de.tum.cit.aet.artemis.iris.domain.settings.IrisSubSettings; +import de.tum.cit.aet.artemis.iris.domain.settings.IrisTextExerciseChatSubSettings; import de.tum.cit.aet.artemis.iris.dto.IrisCombinedChatSubSettingsDTO; import de.tum.cit.aet.artemis.iris.dto.IrisCombinedCompetencyGenerationSubSettingsDTO; -import de.tum.cit.aet.artemis.iris.dto.IrisCombinedHestiaSubSettingsDTO; import de.tum.cit.aet.artemis.iris.dto.IrisCombinedLectureIngestionSubSettingsDTO; +import de.tum.cit.aet.artemis.iris.dto.IrisCombinedTextExerciseChatSubSettingsDTO; /** * Service for handling {@link IrisSubSettings} objects. @@ -76,75 +75,73 @@ public IrisChatSubSettings update(IrisChatSubSettings currentSettings, IrisChatS currentSettings.setRateLimit(newSettings.getRateLimit()); currentSettings.setRateLimitTimeframeHours(newSettings.getRateLimitTimeframeHours()); } - currentSettings.setAllowedModels(selectAllowedModels(currentSettings.getAllowedModels(), newSettings.getAllowedModels())); - currentSettings.setPreferredModel(validatePreferredModel(currentSettings.getPreferredModel(), newSettings.getPreferredModel(), currentSettings.getAllowedModels(), - parentSettings != null ? parentSettings.allowedModels() : null)); - currentSettings.setTemplate(newSettings.getTemplate()); + currentSettings.setAllowedVariants(selectAllowedVariants(currentSettings.getAllowedVariants(), newSettings.getAllowedVariants())); + currentSettings.setSelectedVariant(validateSelectedVariant(currentSettings.getSelectedVariant(), newSettings.getSelectedVariant(), currentSettings.getAllowedVariants(), + parentSettings != null ? parentSettings.allowedVariants() : null)); return currentSettings; } /** - * Updates a Lecture Ingestion sub settings object. - * If the new settings are null, the current settings will be deleted (except if the parent settings are null == if the settings are global). - * Special notes: + * Updates a text exercise chat sub settings object. * - * @param currentSettings Current Lecture Ingestion sub settings. - * @param newSettings Updated Lecture Ingestion sub settings. - * @param parentSettings Parent Lecture Ingestion sub settings. + * @param currentSettings Current chat sub settings. + * @param newSettings Updated chat sub settings. + * @param parentSettings Parent chat sub settings. * @param settingsType Type of the settings the sub settings belong to. - * @return Updated Lecture Ingestion sub settings. + * @return Updated chat sub settings. */ - public IrisLectureIngestionSubSettings update(IrisLectureIngestionSubSettings currentSettings, IrisLectureIngestionSubSettings newSettings, - IrisCombinedLectureIngestionSubSettingsDTO parentSettings, IrisSettingsType settingsType) { + public IrisTextExerciseChatSubSettings update(IrisTextExerciseChatSubSettings currentSettings, IrisTextExerciseChatSubSettings newSettings, + IrisCombinedTextExerciseChatSubSettingsDTO parentSettings, IrisSettingsType settingsType) { if (newSettings == null) { if (parentSettings == null) { - throw new IllegalArgumentException("Cannot delete the Lecture Ingestion settings"); + throw new IllegalArgumentException("Cannot delete the chat settings"); } return null; } if (currentSettings == null) { - currentSettings = new IrisLectureIngestionSubSettings(); + currentSettings = new IrisTextExerciseChatSubSettings(); } - - if (authCheckService.isAdmin() && (settingsType == IrisSettingsType.COURSE || settingsType == IrisSettingsType.GLOBAL)) { + if (settingsType == IrisSettingsType.EXERCISE || authCheckService.isAdmin()) { currentSettings.setEnabled(newSettings.isEnabled()); - currentSettings.setAutoIngestOnLectureAttachmentUpload(newSettings.getAutoIngestOnLectureAttachmentUpload()); } - + if (authCheckService.isAdmin()) { + currentSettings.setRateLimit(newSettings.getRateLimit()); + currentSettings.setRateLimitTimeframeHours(newSettings.getRateLimitTimeframeHours()); + } + currentSettings.setAllowedVariants(selectAllowedVariants(currentSettings.getAllowedVariants(), newSettings.getAllowedVariants())); + currentSettings.setSelectedVariant(validateSelectedVariant(currentSettings.getSelectedVariant(), newSettings.getSelectedVariant(), currentSettings.getAllowedVariants(), + parentSettings != null ? parentSettings.allowedVariants() : null)); return currentSettings; } /** - * Updates a Hestia sub settings object. + * Updates a Lecture Ingestion sub settings object. * If the new settings are null, the current settings will be deleted (except if the parent settings are null == if the settings are global). * Special notes: - * - If the user is not an admin the allowed models will not be updated. - * - If the user is not an admin the preferred model will only be updated if it is included in the allowed models. * - * @param currentSettings Current Hestia sub settings. - * @param newSettings Updated Hestia sub settings. - * @param parentSettings Parent Hestia sub settings. + * @param currentSettings Current Lecture Ingestion sub settings. + * @param newSettings Updated Lecture Ingestion sub settings. + * @param parentSettings Parent Lecture Ingestion sub settings. * @param settingsType Type of the settings the sub settings belong to. - * @return Updated Hestia sub settings. + * @return Updated Lecture Ingestion sub settings. */ - public IrisHestiaSubSettings update(IrisHestiaSubSettings currentSettings, IrisHestiaSubSettings newSettings, IrisCombinedHestiaSubSettingsDTO parentSettings, - IrisSettingsType settingsType) { + public IrisLectureIngestionSubSettings update(IrisLectureIngestionSubSettings currentSettings, IrisLectureIngestionSubSettings newSettings, + IrisCombinedLectureIngestionSubSettingsDTO parentSettings, IrisSettingsType settingsType) { if (newSettings == null) { if (parentSettings == null) { - throw new IllegalArgumentException("Cannot delete the Hestia settings"); + throw new IllegalArgumentException("Cannot delete the Lecture Ingestion settings"); } return null; } if (currentSettings == null) { - currentSettings = new IrisHestiaSubSettings(); + currentSettings = new IrisLectureIngestionSubSettings(); } - if (settingsType == IrisSettingsType.EXERCISE || authCheckService.isAdmin()) { + + if (authCheckService.isAdmin() && (settingsType == IrisSettingsType.COURSE || settingsType == IrisSettingsType.GLOBAL)) { currentSettings.setEnabled(newSettings.isEnabled()); + currentSettings.setAutoIngestOnLectureAttachmentUpload(newSettings.getAutoIngestOnLectureAttachmentUpload()); } - currentSettings.setAllowedModels(selectAllowedModels(currentSettings.getAllowedModels(), newSettings.getAllowedModels())); - currentSettings.setPreferredModel(validatePreferredModel(currentSettings.getPreferredModel(), newSettings.getPreferredModel(), currentSettings.getAllowedModels(), - parentSettings != null ? parentSettings.allowedModels() : null)); - currentSettings.setTemplate(newSettings.getTemplate()); + return currentSettings; } @@ -174,11 +171,10 @@ public IrisCompetencyGenerationSubSettings update(IrisCompetencyGenerationSubSet } if (authCheckService.isAdmin()) { currentSettings.setEnabled(newSettings.isEnabled()); - currentSettings.setAllowedModels(selectAllowedModels(currentSettings.getAllowedModels(), newSettings.getAllowedModels())); + currentSettings.setAllowedVariants(selectAllowedVariants(currentSettings.getAllowedVariants(), newSettings.getAllowedVariants())); } - currentSettings.setPreferredModel(validatePreferredModel(currentSettings.getPreferredModel(), newSettings.getPreferredModel(), currentSettings.getAllowedModels(), - parentSettings != null ? parentSettings.allowedModels() : null)); - currentSettings.setTemplate(newSettings.getTemplate()); + currentSettings.setSelectedVariant(validateSelectedVariant(currentSettings.getSelectedVariant(), newSettings.getSelectedVariant(), currentSettings.getAllowedVariants(), + parentSettings != null ? parentSettings.allowedVariants() : null)); return currentSettings; } @@ -187,12 +183,12 @@ public IrisCompetencyGenerationSubSettings update(IrisCompetencyGenerationSubSet * If the user is an admin, all models are allowed. * Otherwise, only models that are allowed by the parent settings or the current settings are allowed. * - * @param allowedModels The allowed models of the current settings. - * @param updatedAllowedModels The allowed models of the updated settings. + * @param allowedVariants The allowed models of the current settings. + * @param updatedAllowedVariants The allowed models of the updated settings. * @return The filtered allowed models. */ - private SortedSet selectAllowedModels(SortedSet allowedModels, SortedSet updatedAllowedModels) { - return authCheckService.isAdmin() ? updatedAllowedModels : allowedModels; + private SortedSet selectAllowedVariants(SortedSet allowedVariants, SortedSet updatedAllowedVariants) { + return authCheckService.isAdmin() ? updatedAllowedVariants : allowedVariants; } /** @@ -200,23 +196,23 @@ private SortedSet selectAllowedModels(SortedSet allowedModels, S * If the user is an admin, all models are allowed. * Otherwise, only models that are allowed by the current settings are allowed. * - * @param preferredModel The preferred model of the current settings. - * @param newPreferredModel The preferred model of the updated settings. - * @param allowedModels The allowed models of the current settings. - * @param parentAllowedModels The allowed models of the parent settings. + * @param selectedVariant The preferred model of the current settings. + * @param newSelectedVariant The preferred model of the updated settings. + * @param allowedVariants The allowed models of the current settings. + * @param parentAllowedVariants The allowed models of the parent settings. * @return The validated preferred model. */ - private String validatePreferredModel(String preferredModel, String newPreferredModel, Set allowedModels, Set parentAllowedModels) { - if (newPreferredModel == null || newPreferredModel.isBlank()) { + private String validateSelectedVariant(String selectedVariant, String newSelectedVariant, Set allowedVariants, Set parentAllowedVariants) { + if (newSelectedVariant == null || newSelectedVariant.isBlank()) { return null; } - var canChangePreferredModel = authCheckService.isAdmin() || (allowedModels != null && !allowedModels.isEmpty() && allowedModels.contains(newPreferredModel)) - || ((allowedModels == null || allowedModels.isEmpty()) && parentAllowedModels != null && parentAllowedModels.contains(newPreferredModel)); - if (canChangePreferredModel) { - return newPreferredModel; + var canChangeSelectedVariant = authCheckService.isAdmin() || (allowedVariants != null && !allowedVariants.isEmpty() && allowedVariants.contains(newSelectedVariant)) + || ((allowedVariants == null || allowedVariants.isEmpty()) && parentAllowedVariants != null && parentAllowedVariants.contains(newSelectedVariant)); + if (canChangeSelectedVariant) { + return newSelectedVariant; } - return preferredModel; + return selectedVariant; } /** @@ -228,45 +224,43 @@ private String validatePreferredModel(String preferredModel, String newPreferred * @param minimal Whether to return a minimal version of the combined settings. * @return Combined chat settings. */ - public IrisCombinedChatSubSettingsDTO combineChatSettings(ArrayList settingsList, boolean minimal) { - var enabled = getCombinedEnabled(settingsList, IrisSettings::getIrisChatSettings); + public IrisCombinedTextExerciseChatSubSettingsDTO combineTextExerciseChatSettings(ArrayList settingsList, boolean minimal) { + var enabled = getCombinedEnabled(settingsList, IrisSettings::getIrisTextExerciseChatSettings); var rateLimit = getCombinedRateLimit(settingsList); - var allowedModels = minimal ? getCombinedAllowedModels(settingsList, IrisSettings::getIrisChatSettings) : null; - var preferredModel = minimal ? getCombinedPreferredModel(settingsList, IrisSettings::getIrisChatSettings) : null; - var template = minimal ? getCombinedTemplate(settingsList, IrisSettings::getIrisChatSettings, IrisChatSubSettings::getTemplate) : null; - return new IrisCombinedChatSubSettingsDTO(enabled, rateLimit, null, allowedModels, preferredModel, template); + var allowedVariants = !minimal ? getCombinedAllowedVariants(settingsList, IrisSettings::getIrisChatSettings) : null; + var selectedVariant = !minimal ? getCombinedSelectedVariant(settingsList, IrisSettings::getIrisChatSettings) : null; + return new IrisCombinedTextExerciseChatSubSettingsDTO(enabled, rateLimit, null, allowedVariants, selectedVariant); } /** - * Combines the Lecture Ingestion settings of multiple {@link IrisSettings} objects. + * Combines the chat settings of multiple {@link IrisSettings} objects. * If minimal is true, the returned object will only contain the enabled and rateLimit fields. * The minimal version can safely be sent to students. * * @param settingsList List of {@link IrisSettings} objects to combine. * @param minimal Whether to return a minimal version of the combined settings. - * @return Combined Lecture Ingestion settings. + * @return Combined chat settings. */ - public IrisCombinedLectureIngestionSubSettingsDTO combineLectureIngestionSubSettings(ArrayList settingsList, boolean minimal) { - var enabled = getCombinedEnabled(settingsList, IrisSettings::getIrisLectureIngestionSettings); - return new IrisCombinedLectureIngestionSubSettingsDTO(enabled); + public IrisCombinedChatSubSettingsDTO combineChatSettings(ArrayList settingsList, boolean minimal) { + var enabled = getCombinedEnabled(settingsList, IrisSettings::getIrisChatSettings); + var rateLimit = getCombinedRateLimit(settingsList); + var allowedVariants = !minimal ? getCombinedAllowedVariants(settingsList, IrisSettings::getIrisChatSettings) : null; + var selectedVariant = !minimal ? getCombinedSelectedVariant(settingsList, IrisSettings::getIrisChatSettings) : null; + return new IrisCombinedChatSubSettingsDTO(enabled, rateLimit, null, allowedVariants, selectedVariant); } /** - * Combines the Hestia settings of multiple {@link IrisSettings} objects. - * If minimal is true, the returned object will only contain the enabled field. + * Combines the Lecture Ingestion settings of multiple {@link IrisSettings} objects. + * If minimal is true, the returned object will only contain the enabled and rateLimit fields. * The minimal version can safely be sent to students. * * @param settingsList List of {@link IrisSettings} objects to combine. * @param minimal Whether to return a minimal version of the combined settings. - * @return Combined Hestia settings. + * @return Combined Lecture Ingestion settings. */ - public IrisCombinedHestiaSubSettingsDTO combineHestiaSettings(ArrayList settingsList, boolean minimal) { - var actualSettingsList = settingsList.stream().filter(settings -> !(settings instanceof IrisExerciseSettings)).toList(); - var enabled = getCombinedEnabled(actualSettingsList, IrisSettings::getIrisHestiaSettings); - var allowedModels = minimal ? getCombinedAllowedModels(actualSettingsList, IrisSettings::getIrisHestiaSettings) : null; - var preferredModel = minimal ? getCombinedPreferredModel(actualSettingsList, IrisSettings::getIrisHestiaSettings) : null; - var template = minimal ? getCombinedTemplate(actualSettingsList, IrisSettings::getIrisHestiaSettings, IrisHestiaSubSettings::getTemplate) : null; - return new IrisCombinedHestiaSubSettingsDTO(enabled, allowedModels, preferredModel, template); + public IrisCombinedLectureIngestionSubSettingsDTO combineLectureIngestionSubSettings(ArrayList settingsList, boolean minimal) { + var enabled = getCombinedEnabled(settingsList, IrisSettings::getIrisLectureIngestionSettings); + return new IrisCombinedLectureIngestionSubSettingsDTO(enabled); } /** @@ -281,11 +275,9 @@ public IrisCombinedHestiaSubSettingsDTO combineHestiaSettings(ArrayList settingsList, boolean minimal) { var actualSettingsList = settingsList.stream().filter(settings -> !(settings instanceof IrisExerciseSettings)).toList(); var enabled = getCombinedEnabled(actualSettingsList, IrisSettings::getIrisCompetencyGenerationSettings); - var allowedModels = minimal ? getCombinedAllowedModels(actualSettingsList, IrisSettings::getIrisCompetencyGenerationSettings) : null; - var preferredModel = minimal ? getCombinedPreferredModel(actualSettingsList, IrisSettings::getIrisCompetencyGenerationSettings) : null; - var template = minimal ? getCombinedTemplate(actualSettingsList, IrisSettings::getIrisCompetencyGenerationSettings, IrisCompetencyGenerationSubSettings::getTemplate) - : null; - return new IrisCombinedCompetencyGenerationSubSettingsDTO(enabled, allowedModels, preferredModel, template); + var allowedVariants = !minimal ? getCombinedAllowedVariants(actualSettingsList, IrisSettings::getIrisCompetencyGenerationSettings) : null; + var selectedVariant = !minimal ? getCombinedSelectedVariant(actualSettingsList, IrisSettings::getIrisCompetencyGenerationSettings) : null; + return new IrisCombinedCompetencyGenerationSubSettingsDTO(enabled, allowedVariants, selectedVariant); } /** @@ -322,43 +314,28 @@ private Integer getCombinedRateLimit(List settingsList) { } /** - * Combines the allowedModels field of multiple {@link IrisSettings} objects. - * Simply takes the last allowedModels. + * Combines the allowedVariants field of multiple {@link IrisSettings} objects. + * Simply takes the last allowedVariants. * * @param settingsList List of {@link IrisSettings} objects to combine. * @param subSettingsFunction Function to get the sub settings from an IrisSettings object. - * @return Combined allowedModels field. + * @return Combined allowedVariants field. */ - private Set getCombinedAllowedModels(List settingsList, Function subSettingsFunction) { - return settingsList.stream().filter(Objects::nonNull).map(subSettingsFunction).filter(Objects::nonNull).map(IrisSubSettings::getAllowedModels).filter(Objects::nonNull) + private Set getCombinedAllowedVariants(List settingsList, Function subSettingsFunction) { + return settingsList.stream().filter(Objects::nonNull).map(subSettingsFunction).filter(Objects::nonNull).map(IrisSubSettings::getAllowedVariants).filter(Objects::nonNull) .filter(models -> !models.isEmpty()).reduce((first, second) -> second).orElse(new TreeSet<>()); } /** - * Combines the preferredModel field of multiple {@link IrisSettings} objects. - * Simply takes the last preferredModel. - * TODO + * Combines the selectedVariant field of multiple {@link IrisSettings} objects. + * Simply takes the last selectedVariant. * * @param settingsList List of {@link IrisSettings} objects to combine. * @param subSettingsFunction Function to get the sub settings from an IrisSettings object. - * @return Combined preferredModel field. + * @return Combined selectedVariant field. */ - private String getCombinedPreferredModel(List settingsList, Function subSettingsFunction) { - return settingsList.stream().filter(Objects::nonNull).map(subSettingsFunction).filter(Objects::nonNull).map(IrisSubSettings::getPreferredModel) + private String getCombinedSelectedVariant(List settingsList, Function subSettingsFunction) { + return settingsList.stream().filter(Objects::nonNull).map(subSettingsFunction).filter(Objects::nonNull).map(IrisSubSettings::getSelectedVariant) .filter(model -> model != null && !model.isBlank()).reduce((first, second) -> second).orElse(null); } - - /** - * Combines the template field of multiple {@link IrisSettings} objects. - * Simply takes the last template. - * - * @param settingsList List of {@link IrisSettings} objects to combine. - * @param templateFunction Function to get the template from the sub settings from an IrisSettings object. - * @return Combined template field. - */ - private IrisTemplate getCombinedTemplate(List settingsList, Function subSettingsFunction, - Function templateFunction) { - return settingsList.stream().filter(Objects::nonNull).map(subSettingsFunction).filter(Objects::nonNull).map(templateFunction) - .filter(template -> template != null && template.getContent() != null && !template.getContent().isBlank()).reduce((first, second) -> second).orElse(null); - } } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisExerciseChatSessionResource.java b/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisExerciseChatSessionResource.java index 55204aa63397..9e736ce8c358 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisExerciseChatSessionResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisExerciseChatSessionResource.java @@ -35,6 +35,7 @@ @Profile(PROFILE_IRIS) @RestController @RequestMapping("api/iris/exercise-chat/") +// TODO: Rename to IrisProgrammingExerciseChatSessionResource public class IrisExerciseChatSessionResource { protected final UserRepository userRepository; @@ -47,6 +48,7 @@ public class IrisExerciseChatSessionResource { protected final IrisRateLimitService irisRateLimitService; + // TODO: This could be a ProgrammingExerciseRepository protected final ExerciseRepository exerciseRepository; private final IrisExerciseChatSessionRepository irisExerciseChatSessionRepository; @@ -134,6 +136,7 @@ public ResponseEntity createSessionForExercise(@PathVar private static ProgrammingExercise validateExercise(Exercise exercise) { if (!(exercise instanceof ProgrammingExercise programmingExercise)) { + // TODO: Remove this once we are only fetching ProgrammingExercises from the DB anyway throw new ConflictException("Iris is only supported for programming exercises", "Iris", "irisProgrammingExercise"); } if (exercise.isExamExercise()) { diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisModelsResource.java b/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisModelsResource.java deleted file mode 100644 index ae4bedb82493..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisModelsResource.java +++ /dev/null @@ -1,49 +0,0 @@ -package de.tum.cit.aet.artemis.iris.web; - -import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_IRIS; - -import java.util.List; - -import org.springframework.context.annotation.Profile; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import de.tum.cit.aet.artemis.core.exception.InternalServerErrorException; -import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastEditor; -import de.tum.cit.aet.artemis.iris.service.pyris.PyrisConnectorException; -import de.tum.cit.aet.artemis.iris.service.pyris.PyrisConnectorService; -import de.tum.cit.aet.artemis.iris.service.pyris.dto.PyrisModelDTO; - -/** - * REST controller for managing the models Pyris provides. - */ -@Profile(PROFILE_IRIS) -@RestController -@RequestMapping("api/") -public class IrisModelsResource { - - private final PyrisConnectorService pyrisConnectorService; - - public IrisModelsResource(PyrisConnectorService pyrisConnectorService) { - this.pyrisConnectorService = pyrisConnectorService; - } - - /** - * GET iris/models: Retrieve all available models offered by Pyris - * - * @return the {@link ResponseEntity} with status {@code 200 (Ok)} and with body a List of the models - */ - @GetMapping("iris/models") - @EnforceAtLeastEditor - public ResponseEntity> getAllModels() { - try { - var models = pyrisConnectorService.getOfferedModels(); - return ResponseEntity.ok(models); - } - catch (PyrisConnectorException e) { - throw new InternalServerErrorException("Could not fetch available Iris models"); - } - } -} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisSettingsResource.java b/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisSettingsResource.java index 32676da073bb..4f2c5416211d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisSettingsResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisSettingsResource.java @@ -14,16 +14,20 @@ 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.Role; -import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastEditor; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastInstructor; -import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; +import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInCourse.EnforceAtLeastEditorInCourse; +import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInCourse.EnforceAtLeastInstructorInCourse; +import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInCourse.EnforceAtLeastStudentInCourse; +import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInExercise.EnforceAtLeastEditorInExercise; +import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInExercise.EnforceAtLeastInstructorInExercise; +import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInExercise.EnforceAtLeastStudentInExercise; import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; +import de.tum.cit.aet.artemis.exercise.repository.ExerciseRepository; import de.tum.cit.aet.artemis.iris.domain.settings.IrisCourseSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisExerciseSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisSettings; import de.tum.cit.aet.artemis.iris.dto.IrisCombinedSettingsDTO; import de.tum.cit.aet.artemis.iris.service.settings.IrisSettingsService; -import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseRepository; /** * REST controller for managing {@link IrisSettings}. @@ -41,15 +45,15 @@ public class IrisSettingsResource { private final AuthorizationCheckService authCheckService; - private final ProgrammingExerciseRepository programmingExerciseRepository; + private final ExerciseRepository exerciseRepository; public IrisSettingsResource(UserRepository userRepository, CourseRepository courseRepository, IrisSettingsService irisSettingsService, - AuthorizationCheckService authCheckService, ProgrammingExerciseRepository programmingExerciseRepository) { + AuthorizationCheckService authCheckService, ExerciseRepository exerciseRepository) { this.userRepository = userRepository; this.courseRepository = courseRepository; this.irisSettingsService = irisSettingsService; this.authCheckService = authCheckService; - this.programmingExerciseRepository = programmingExerciseRepository; + this.exerciseRepository = exerciseRepository; } /** @@ -71,24 +75,23 @@ public ResponseEntity getGlobalSettings() { * @return the {@link ResponseEntity} with status {@code 200 (Ok)} and with body the settings, or with status {@code 404 (Not Found)} if the course could not be found. */ @GetMapping("courses/{courseId}/raw-iris-settings") - @EnforceAtLeastEditor + @EnforceAtLeastEditorInCourse public ResponseEntity getRawCourseSettings(@PathVariable Long courseId) { var course = courseRepository.findByIdElseThrow(courseId); - authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, course, null); var irisSettings = irisSettingsService.getRawIrisSettingsFor(course); return ResponseEntity.ok(irisSettings); } /** - * GET programming-exercises/{exerciseId}/raw-iris-settings: Retrieve the raw iris settings for the programming exercise. + * GET exercises/{exerciseId}/raw-iris-settings: Retrieve the raw iris settings for the exercise. * - * @param exerciseId of the programming exercise + * @param exerciseId of the exercise * @return the {@link ResponseEntity} with status {@code 200 (Ok)} and with body the settings, or with status {@code 404 (Not Found)} if the exercise could not be found. */ - @GetMapping("programming-exercises/{exerciseId}/raw-iris-settings") - @EnforceAtLeastEditor - public ResponseEntity getRawProgrammingExerciseSettings(@PathVariable Long exerciseId) { - var exercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); + @GetMapping("exercises/{exerciseId}/raw-iris-settings") + @EnforceAtLeastEditorInExercise + public ResponseEntity getRawExerciseSettings(@PathVariable Long exerciseId) { + var exercise = exerciseRepository.findByIdElseThrow(exerciseId); var user = userRepository.getUserWithGroupsAndAuthorities(); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.STUDENT, exercise, user); @@ -103,11 +106,10 @@ public ResponseEntity getRawProgrammingExerciseSettings(@PathVaria * @return the {@link ResponseEntity} with status {@code 200 (Ok)} and with body the settings, or with status {@code 404 (Not Found)} if the course could not be found. */ @GetMapping("courses/{courseId}/iris-settings") - @EnforceAtLeastStudent + @EnforceAtLeastStudentInCourse public ResponseEntity getCourseSettings(@PathVariable Long courseId) { var course = courseRepository.findByIdElseThrow(courseId); var user = userRepository.getUserWithGroupsAndAuthorities(); - authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, user); // Editors can see the full settings, students only the reduced settings var getReduced = !authCheckService.isAtLeastEditorInCourse(course, user); @@ -116,17 +118,16 @@ public ResponseEntity getCourseSettings(@PathVariable L } /** - * GET programming-exercises/{exerciseId}/iris-settings: Retrieve the actual iris settings for the programming exercise. + * GET exercises/{exerciseId}/iris-settings: Retrieve the actual iris settings for the exercise. * - * @param exerciseId of the programming exercise + * @param exerciseId of the exercise * @return the {@link ResponseEntity} with status {@code 200 (Ok)} and with body the settings, or with status {@code 404 (Not Found)} if the exercise could not be found. */ - @GetMapping("programming-exercises/{exerciseId}/iris-settings") - @EnforceAtLeastStudent - public ResponseEntity getProgrammingExerciseSettings(@PathVariable Long exerciseId) { - var exercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); + @GetMapping("exercises/{exerciseId}/iris-settings") + @EnforceAtLeastStudentInExercise + public ResponseEntity getExerciseSettings(@PathVariable Long exerciseId) { + var exercise = exerciseRepository.findByIdElseThrow(exerciseId); var user = userRepository.getUserWithGroupsAndAuthorities(); - authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.STUDENT, exercise, user); var combinedIrisSettings = irisSettingsService.getCombinedIrisSettingsFor(exercise, irisSettingsService.shouldShowMinimalSettings(exercise, user)); return ResponseEntity.ok(combinedIrisSettings); @@ -140,29 +141,26 @@ public ResponseEntity getProgrammingExerciseSettings(@P * @return the {@link ResponseEntity} with status {@code 200 (Ok)} and with body the updated settings, or with status {@code 404 (Not Found)} if the course could not be found. */ @PutMapping("courses/{courseId}/raw-iris-settings") - @EnforceAtLeastEditor + @EnforceAtLeastInstructorInCourse public ResponseEntity updateCourseSettings(@PathVariable Long courseId, @RequestBody IrisCourseSettings settings) { var course = courseRepository.findByIdElseThrow(courseId); - authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, course, null); settings.setCourse(course); var updatedSettings = irisSettingsService.saveIrisSettings(settings); return ResponseEntity.ok(updatedSettings); } /** - * PUT programming-exercises/{exerciseId}/raw-iris-settings: Update the raw iris settings for the programming exercise. + * PUT exercises/{exerciseId}/raw-iris-settings: Update the raw iris settings for the exercise. * - * @param exerciseId of the programming exercise + * @param exerciseId of the exercise * @param settings the settings to update * @return the {@link ResponseEntity} with status {@code 200 (Ok)} and with body the updated settings, or with status {@code 404 (Not Found)} if the exercise could not be * found. */ - @PutMapping("programming-exercises/{exerciseId}/raw-iris-settings") - @EnforceAtLeastInstructor - public ResponseEntity updateProgrammingExerciseSettings(@PathVariable Long exerciseId, @RequestBody IrisExerciseSettings settings) { - var exercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); - var user = userRepository.getUserWithGroupsAndAuthorities(); - authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.INSTRUCTOR, exercise, user); + @PutMapping("exercises/{exerciseId}/raw-iris-settings") + @EnforceAtLeastInstructorInExercise + public ResponseEntity updateExerciseSettings(@PathVariable Long exerciseId, @RequestBody IrisExerciseSettings settings) { + var exercise = exerciseRepository.findByIdElseThrow(exerciseId); settings.setExercise(exercise); var updatedSettings = irisSettingsService.saveIrisSettings(settings); return ResponseEntity.ok(updatedSettings); diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisTextExerciseChatSessionResource.java b/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisTextExerciseChatSessionResource.java new file mode 100644 index 000000000000..9ebb7aa647a2 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisTextExerciseChatSessionResource.java @@ -0,0 +1,137 @@ +package de.tum.cit.aet.artemis.iris.web; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; + +import org.springframework.context.annotation.Profile; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import de.tum.cit.aet.artemis.core.exception.ConflictException; +import de.tum.cit.aet.artemis.core.repository.UserRepository; +import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInExercise.EnforceAtLeastStudentInExercise; +import de.tum.cit.aet.artemis.iris.domain.session.IrisTextExerciseChatSession; +import de.tum.cit.aet.artemis.iris.domain.settings.IrisSubSettingsType; +import de.tum.cit.aet.artemis.iris.repository.IrisTextExerciseChatSessionRepository; +import de.tum.cit.aet.artemis.iris.service.IrisSessionService; +import de.tum.cit.aet.artemis.iris.service.session.IrisTextExerciseChatSessionService; +import de.tum.cit.aet.artemis.iris.service.settings.IrisSettingsService; +import de.tum.cit.aet.artemis.text.domain.TextExercise; +import de.tum.cit.aet.artemis.text.repository.TextExerciseRepository; + +/** + * REST controller for managing {@link IrisTextExerciseChatSession}. + */ +@Profile("iris") +@RestController +@RequestMapping("api/iris/text-exercise-chat/") +public class IrisTextExerciseChatSessionResource { + + private final UserRepository userRepository; + + private final IrisSessionService irisSessionService; + + private final IrisSettingsService irisSettingsService; + + private final TextExerciseRepository textExerciseRepository; + + private final IrisTextExerciseChatSessionService irisTextExerciseChatSessionService; + + private final IrisTextExerciseChatSessionRepository irisTextExerciseChatSessionRepository; + + protected IrisTextExerciseChatSessionResource(IrisTextExerciseChatSessionRepository irisTextExerciseChatSessionRepository, UserRepository userRepository, + TextExerciseRepository textExerciseRepository, IrisSessionService irisSessionService, IrisSettingsService irisSettingsService, + IrisTextExerciseChatSessionService irisTextExerciseChatSessionService) { + this.irisTextExerciseChatSessionRepository = irisTextExerciseChatSessionRepository; + this.userRepository = userRepository; + this.irisSessionService = irisSessionService; + this.irisSettingsService = irisSettingsService; + this.textExerciseRepository = textExerciseRepository; + this.irisTextExerciseChatSessionService = irisTextExerciseChatSessionService; + } + + /** + * GET exercise-chat/{exerciseId}/sessions/current: Retrieve the current iris session for the programming exercise. + * + * @param exerciseId of the exercise + * @return the {@link ResponseEntity} with status {@code 200 (Ok)} and with body the current iris session for the exercise or {@code 404 (Not Found)} if no session exists + */ + @PostMapping("{exerciseId}/sessions/current") + @EnforceAtLeastStudentInExercise + public ResponseEntity getCurrentSessionOrCreateIfNotExists(@PathVariable Long exerciseId) throws URISyntaxException { + var exercise = textExerciseRepository.findByIdElseThrow(exerciseId); + validateExercise(exercise); + + irisSettingsService.isEnabledForElseThrow(IrisSubSettingsType.TEXT_EXERCISE_CHAT, exercise); + var user = userRepository.getUserWithGroupsAndAuthorities(); + + var sessionOptional = irisTextExerciseChatSessionRepository.findLatestByExerciseIdAndUserIdWithMessages(exercise.getId(), user.getId(), Pageable.ofSize(1)).stream() + .findFirst(); + if (sessionOptional.isPresent()) { + var session = sessionOptional.get(); + irisSessionService.checkHasAccessToIrisSession(session, user); + return ResponseEntity.ok(session); + } + + return createSessionForExercise(exerciseId); + } + + /** + * POST exercise-chat/{exerciseId}/session: Create a new iris session for an exercise and user. + * If there already exists an iris session for the exercise and user, a new one is created. + * Note: The old session including messages is not deleted and can still be retrieved + * + * @param exerciseId of the exercise + * @return the {@link ResponseEntity} with status {@code 200 (Ok)} and with body the new iris session for the exercise + */ + @PostMapping("{exerciseId}/sessions") + @EnforceAtLeastStudentInExercise + public ResponseEntity createSessionForExercise(@PathVariable Long exerciseId) throws URISyntaxException { + var textExercise = textExerciseRepository.findByIdElseThrow(exerciseId); + validateExercise(textExercise); + + irisSettingsService.isEnabledForElseThrow(IrisSubSettingsType.TEXT_EXERCISE_CHAT, textExercise); + var user = userRepository.getUserWithGroupsAndAuthorities(); + user.hasAcceptedIrisElseThrow(); + + var session = irisTextExerciseChatSessionRepository.save(new IrisTextExerciseChatSession(textExercise, user)); + var uriString = "/api/iris/sessions/" + session.getId(); + + return ResponseEntity.created(new URI(uriString)).body(session); + } + + /** + * GET exercise-chat/{exerciseId}/sessions: Retrieve all Iris Sessions for the programming exercise + * + * @param exerciseId of the exercise + * @return the {@link ResponseEntity} with status {@code 200 (Ok)} and with body a list of the iris sessions for the exercise or {@code 404 (Not Found)} if no session exists + */ + @GetMapping("{exerciseId}/sessions") + @EnforceAtLeastStudentInExercise + public ResponseEntity> getAllSessions(@PathVariable Long exerciseId) { + var exercise = textExerciseRepository.findByIdElseThrow(exerciseId); + validateExercise(exercise); + + irisSettingsService.isEnabledForElseThrow(IrisSubSettingsType.TEXT_EXERCISE_CHAT, exercise); + var user = userRepository.getUserWithGroupsAndAuthorities(); + user.hasAcceptedIrisElseThrow(); + + var sessions = irisTextExerciseChatSessionRepository.findByExerciseIdAndUserIdElseThrow(exercise.getId(), user.getId()); + // TODO: Discuss this with the team: should we filter out sessions where the user does not have access, or throw an exception? + // Access check might not even be necessary here -> see comments in hasAccess method + var filteredSessions = sessions.stream().filter(session -> irisTextExerciseChatSessionService.hasAccess(user, session)).toList(); + return ResponseEntity.ok(filteredSessions); + } + + private static void validateExercise(TextExercise exercise) { + if (exercise.isExamExercise()) { + throw new ConflictException("Iris is not supported for exam exercises", "Iris", "irisExamExercise"); + } + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisVariantsResource.java b/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisVariantsResource.java new file mode 100644 index 000000000000..9342d1522023 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisVariantsResource.java @@ -0,0 +1,56 @@ +package de.tum.cit.aet.artemis.iris.web; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import de.tum.cit.aet.artemis.core.exception.InternalServerErrorException; +import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastEditor; +import de.tum.cit.aet.artemis.iris.domain.settings.IrisSubSettingsType; +import de.tum.cit.aet.artemis.iris.service.pyris.PyrisConnectorException; +import de.tum.cit.aet.artemis.iris.service.pyris.PyrisConnectorService; +import de.tum.cit.aet.artemis.iris.service.pyris.dto.PyrisVariantDTO; + +/** + * REST controller for managing the variants Pyris provides. + */ +@Profile("iris") +@RestController +@RequestMapping("api/") +public class IrisVariantsResource { + + private static final Logger log = LoggerFactory.getLogger(IrisVariantsResource.class); + + private final PyrisConnectorService pyrisConnectorService; + + public IrisVariantsResource(PyrisConnectorService pyrisConnectorService) { + this.pyrisConnectorService = pyrisConnectorService; + } + + /** + * GET iris/variants/{feature}: Retrieve all available variants offered by Pyris for a certain feature + * + * @param featureRaw the feature for which to retrieve the variants + * @return the {@link ResponseEntity} with status {@code 200 (Ok)} and with body a List of the variants + */ + @GetMapping("iris/variants/{feature}") + @EnforceAtLeastEditor + public ResponseEntity> getAllVariants(@PathVariable("feature") String featureRaw) { + var feature = IrisSubSettingsType.valueOf(featureRaw.toUpperCase().replace("-", "_")); + try { + var variants = pyrisConnectorService.getOfferedVariants(feature); + return ResponseEntity.ok(variants); + } + catch (PyrisConnectorException e) { + log.error("Could not fetch available variants for feature {}", feature, e); + throw new InternalServerErrorException("Could not fetch available variants for feature " + feature); + } + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/web/admin/AdminIrisSettingsResource.java b/src/main/java/de/tum/cit/aet/artemis/iris/web/admin/AdminIrisSettingsResource.java index b8ea1f92ba31..40da3e5ee431 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/web/admin/AdminIrisSettingsResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/web/admin/AdminIrisSettingsResource.java @@ -17,6 +17,7 @@ * REST controller for managing {@link IrisSettings}. */ @Profile(PROFILE_IRIS) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminIrisSettingsResource { @@ -34,7 +35,6 @@ public AdminIrisSettingsResource(IrisSettingsService irisSettingsService) { * @return the {@link ResponseEntity} with status {@code 200 (Ok)} and with body the updated settings. */ @PutMapping("iris/global-iris-settings") - @EnforceAdmin public ResponseEntity updateGlobalSettings(@RequestBody IrisSettings settings) { var updatedSettings = irisSettingsService.saveIrisSettings(settings); return ResponseEntity.ok(updatedSettings); 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 05fecc5a287e..025a6ce4e897 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 @@ -20,6 +20,7 @@ import de.tum.cit.aet.artemis.iris.service.pyris.PyrisJobService; import de.tum.cit.aet.artemis.iris.service.pyris.PyrisStatusUpdateService; 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.job.CompetencyExtractionJob; @@ -27,6 +28,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.TextExerciseChatJob; /** * REST controller for providing Pyris access to Artemis internal data and status updates. @@ -59,7 +61,7 @@ public PublicPyrisStatusUpdateResource(PyrisJobService pyrisJobService, PyrisSta * @throws AccessForbiddenException if the token is invalid * @return a {@link ResponseEntity} with status {@code 200 (OK)} */ - @PostMapping("pipelines/tutor-chat/runs/{runId}/status") // TODO: Rename this to 'exercise-chat' with next breaking Pyris version + @PostMapping("pipelines/tutor-chat/runs/{runId}/status") // TODO: Rename this to 'programming-exercise-chat' with next breaking Pyris version @EnforceNothing public ResponseEntity setStatusOfJob(@PathVariable String runId, @RequestBody PyrisChatStatusUpdateDTO statusUpdateDTO, HttpServletRequest request) { var job = pyrisJobService.getAndAuthenticateJobFromHeaderElseThrow(request, ExerciseChatJob.class); @@ -123,6 +125,30 @@ public ResponseEntity setCompetencyExtractionJobStatus(@PathVariable Strin return ResponseEntity.ok().build(); } + /** + * {@code POST /api/public/pyris/pipelines/text-exercise-chat/runs/{runId}/status} : Set the status of a Text Exercise Chat job. + * + * @param runId the ID of the job + * @param statusUpdateDTO the status update + * @param request the HTTP request + * @return a {@link ResponseEntity} with status {@code 200 (OK)} + * @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 + */ + @PostMapping("pipelines/text-exercise-chat/runs/{runId}/status") + @EnforceNothing + public ResponseEntity respondInTextExerciseChat(@PathVariable String runId, @RequestBody PyrisTextExerciseChatStatusUpdateDTO statusUpdateDTO, + HttpServletRequest request) { + var job = pyrisJobService.getAndAuthenticateJobFromHeaderElseThrow(request, TextExerciseChatJob.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/java/de/tum/cit/aet/artemis/lecture/repository/LectureRepository.java b/src/main/java/de/tum/cit/aet/artemis/lecture/repository/LectureRepository.java index 7ab7df9d479d..621216563727 100644 --- a/src/main/java/de/tum/cit/aet/artemis/lecture/repository/LectureRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/lecture/repository/LectureRepository.java @@ -17,6 +17,7 @@ import org.springframework.stereotype.Repository; import de.tum.cit.aet.artemis.core.dto.CourseContentCount; +import de.tum.cit.aet.artemis.core.exception.NoUniqueQueryException; import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; import de.tum.cit.aet.artemis.lecture.domain.Lecture; @@ -100,6 +101,30 @@ public interface LectureRepository extends ArtemisJpaRepository { """) Optional findByIdWithLectureUnitsAndSlidesAndAttachments(@Param("lectureId") long lectureId); + @Query(""" + SELECT lecture + FROM Lecture lecture + LEFT JOIN FETCH lecture.lectureUnits + WHERE lecture.title = :title AND lecture.course.id = :courseId + """) + Set findAllByTitleAndCourseIdWithLectureUnits(@Param("title") String title, @Param("courseId") long courseId); + + /** + * Finds a lecture by its title and course id and throws a NoUniqueQueryException if multiple lectures are found. + * + * @param title the title of the lecture + * @param courseId the id of the course + * @return the lecture with the given title and course id + * @throws NoUniqueQueryException if multiple lectures are found with the same title + */ + default Optional findUniqueByTitleAndCourseIdWithLectureUnitsElseThrow(String title, long courseId) throws NoUniqueQueryException { + Set allLectures = findAllByTitleAndCourseIdWithLectureUnits(title, courseId); + if (allLectures.size() > 1) { + throw new NoUniqueQueryException("Found multiple lectures with title " + title + " in course with id " + courseId); + } + return allLectures.stream().findFirst(); + } + @SuppressWarnings("PMD.MethodNamingConventions") Page findByTitleIgnoreCaseContainingOrCourse_TitleIgnoreCaseContaining(String partialTitle, String partialCourseTitle, Pageable pageable); diff --git a/src/main/java/de/tum/cit/aet/artemis/lecture/repository/LectureUnitRepository.java b/src/main/java/de/tum/cit/aet/artemis/lecture/repository/LectureUnitRepository.java index fceab652c24d..ee29df83380a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/lecture/repository/LectureUnitRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/lecture/repository/LectureUnitRepository.java @@ -5,6 +5,7 @@ import java.util.Optional; import java.util.Set; +import org.hibernate.NonUniqueResultException; import org.springframework.context.annotation.Profile; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -61,6 +62,27 @@ public interface LectureUnitRepository extends ArtemisJpaRepository findByIdWithCompletedUsers(@Param("lectureUnitId") long lectureUnitId); + /** + * Finds a lecture unit by name, lecture title and course id. Currently, name duplicates are allowed but this method throws an exception if multiple lecture units with the + * same name are found. + * + * @param name the name of the lecture unit + * @param lectureTitle the title of the lecture containing the lecture unit + * @param courseId the id of the course containing the lecture + * @return the lecture unit with the given name, lecture title and course id + * @throws NonUniqueResultException if multiple lecture units with the same name in the same lecture are found + */ + @Query(""" + SELECT lu + FROM LectureUnit lu + LEFT JOIN FETCH lu.competencies + WHERE lu.name = :name + AND lu.lecture.title = :lectureTitle + AND lu.lecture.course.id = :courseId + """) + Optional findByNameAndLectureTitleAndCourseIdWithCompetencies(@Param("name") String name, @Param("lectureTitle") String lectureTitle, + @Param("courseId") long courseId) throws NonUniqueResultException; + default LectureUnit findByIdWithCompletedUsersElseThrow(long lectureUnitId) { return getValueElseThrow(findByIdWithCompletedUsers(lectureUnitId), lectureUnitId); } diff --git a/src/main/java/de/tum/cit/aet/artemis/lecture/service/LectureImportService.java b/src/main/java/de/tum/cit/aet/artemis/lecture/service/LectureImportService.java index 5eca29ad0bc1..4a3e880640de 100644 --- a/src/main/java/de/tum/cit/aet/artemis/lecture/service/LectureImportService.java +++ b/src/main/java/de/tum/cit/aet/artemis/lecture/service/LectureImportService.java @@ -2,11 +2,8 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; -import java.net.URI; -import java.nio.file.Path; import java.util.ArrayList; import java.util.HashSet; -import java.util.List; import java.util.Optional; import java.util.Set; @@ -16,22 +13,12 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import de.tum.cit.aet.artemis.communication.service.conversation.ChannelService; import de.tum.cit.aet.artemis.core.domain.Course; -import de.tum.cit.aet.artemis.core.service.FilePathService; -import de.tum.cit.aet.artemis.core.service.FileService; -import de.tum.cit.aet.artemis.iris.repository.IrisSettingsRepository; -import de.tum.cit.aet.artemis.iris.service.pyris.PyrisWebhookService; import de.tum.cit.aet.artemis.lecture.domain.Attachment; -import de.tum.cit.aet.artemis.lecture.domain.AttachmentUnit; -import de.tum.cit.aet.artemis.lecture.domain.ExerciseUnit; import de.tum.cit.aet.artemis.lecture.domain.Lecture; -import de.tum.cit.aet.artemis.lecture.domain.LectureUnit; -import de.tum.cit.aet.artemis.lecture.domain.OnlineUnit; -import de.tum.cit.aet.artemis.lecture.domain.TextUnit; -import de.tum.cit.aet.artemis.lecture.domain.VideoUnit; import de.tum.cit.aet.artemis.lecture.repository.AttachmentRepository; import de.tum.cit.aet.artemis.lecture.repository.LectureRepository; -import de.tum.cit.aet.artemis.lecture.repository.LectureUnitRepository; @Profile(PROFILE_CORE) @Service @@ -41,39 +28,30 @@ public class LectureImportService { private final LectureRepository lectureRepository; - private final LectureUnitRepository lectureUnitRepository; - private final AttachmentRepository attachmentRepository; - private final Optional pyrisWebhookService; - - private final FileService fileService; - - private final SlideSplitterService slideSplitterService; + private final LectureUnitImportService lectureUnitImportService; - private final Optional irisSettingsRepository; + private final ChannelService channelService; - public LectureImportService(LectureRepository lectureRepository, LectureUnitRepository lectureUnitRepository, AttachmentRepository attachmentRepository, - Optional pyrisWebhookService, FileService fileService, SlideSplitterService slideSplitterService, - Optional irisSettingsRepository) { + public LectureImportService(LectureRepository lectureRepository, AttachmentRepository attachmentRepository, LectureUnitImportService lectureUnitImportService, + ChannelService channelService) { this.lectureRepository = lectureRepository; - this.lectureUnitRepository = lectureUnitRepository; this.attachmentRepository = attachmentRepository; - this.pyrisWebhookService = pyrisWebhookService; - this.fileService = fileService; - this.slideSplitterService = slideSplitterService; - this.irisSettingsRepository = irisSettingsRepository; + this.lectureUnitImportService = lectureUnitImportService; + this.channelService = channelService; } /** * Import the {@code importedLecture} including its lecture units and attachments to the {@code course} * - * @param importedLecture The lecture to be imported - * @param course The course to import to + * @param importedLecture The lecture to be imported + * @param course The course to import to + * @param importLectureUnits Whether to import the lecture units of the lecture * @return The lecture in the new course */ @Transactional // Required to circumvent errors with ordered collection of lecture units - public Lecture importLecture(final Lecture importedLecture, final Course course) { + public Lecture importLecture(final Lecture importedLecture, final Course course, boolean importLectureUnits) { log.debug("Creating a new Lecture based on lecture {}", importedLecture); // Copy the lecture itself to the new course @@ -83,127 +61,32 @@ public Lecture importLecture(final Lecture importedLecture, final Course course) lecture.setStartDate(importedLecture.getStartDate()); lecture.setEndDate(importedLecture.getEndDate()); lecture.setVisibleDate(importedLecture.getVisibleDate()); + lecture.setCourse(course); lecture = lectureRepository.save(lecture); - course.addLectures(lecture); - - log.debug("Importing lecture units from lecture"); - List lectureUnits = new ArrayList<>(); - for (LectureUnit lectureUnit : importedLecture.getLectureUnits()) { - LectureUnit clonedLectureUnit = cloneLectureUnit(lectureUnit, lecture); - if (clonedLectureUnit != null) { - clonedLectureUnit.setLecture(lecture); - lectureUnits.add(clonedLectureUnit); - } + + if (importLectureUnits) { + lectureUnitImportService.importLectureUnits(importedLecture, lecture); + } + else { + importedLecture.setLectureUnits(new ArrayList<>()); } - lecture.setLectureUnits(lectureUnits); - lectureUnitRepository.saveAll(lectureUnits); log.debug("Importing attachments from lecture"); Set attachments = new HashSet<>(); for (Attachment attachment : importedLecture.getAttachments()) { - Attachment clonedAttachment = cloneAttachment(lecture.getId(), attachment); + Attachment clonedAttachment = lectureUnitImportService.importAttachment(lecture.getId(), attachment); clonedAttachment.setLecture(lecture); attachments.add(clonedAttachment); } lecture.setAttachments(attachments); attachmentRepository.saveAll(attachments); - // Send lectures to pyris - if (pyrisWebhookService.isPresent() && irisSettingsRepository.isPresent()) { - pyrisWebhookService.get().autoUpdateAttachmentUnitsInPyris(lecture.getCourse().getId(), - lectureUnits.stream().filter(lectureUnit -> lectureUnit instanceof AttachmentUnit).map(lectureUnit -> (AttachmentUnit) lectureUnit).toList()); - } // Save again to establish the ordered list relationship - return lectureRepository.save(lecture); - } + Lecture savedLecture = lectureRepository.save(lecture); - /** - * This helper function clones the {@code importedLectureUnit} and returns it - * - * @param importedLectureUnit The original lecture unit to be copied - * @param newLecture The new lecture to which the lecture units are appended - * @return The cloned lecture unit - */ - private LectureUnit cloneLectureUnit(final LectureUnit importedLectureUnit, final Lecture newLecture) { - log.debug("Creating a new LectureUnit from lecture unit {}", importedLectureUnit); - - if (importedLectureUnit instanceof TextUnit importedTextUnit) { - TextUnit textUnit = new TextUnit(); - textUnit.setName(importedTextUnit.getName()); - textUnit.setReleaseDate(importedTextUnit.getReleaseDate()); - textUnit.setContent(importedTextUnit.getContent()); - return textUnit; - } - else if (importedLectureUnit instanceof VideoUnit importedVideoUnit) { - VideoUnit videoUnit = new VideoUnit(); - videoUnit.setName(importedVideoUnit.getName()); - videoUnit.setReleaseDate(importedVideoUnit.getReleaseDate()); - videoUnit.setDescription(importedVideoUnit.getDescription()); - videoUnit.setSource(importedVideoUnit.getSource()); - return videoUnit; - } - else if (importedLectureUnit instanceof AttachmentUnit importedAttachmentUnit) { - // Create and save the attachment unit, then the attachment itself, as the id is needed for file handling - AttachmentUnit attachmentUnit = new AttachmentUnit(); - attachmentUnit.setDescription(importedAttachmentUnit.getDescription()); - attachmentUnit.setLecture(newLecture); - lectureUnitRepository.save(attachmentUnit); - - Attachment attachment = cloneAttachment(attachmentUnit.getId(), importedAttachmentUnit.getAttachment()); - attachment.setAttachmentUnit(attachmentUnit); - attachmentRepository.save(attachment); - if (attachment.getLink().endsWith(".pdf")) { - slideSplitterService.splitAttachmentUnitIntoSingleSlides(attachmentUnit); - } - attachmentUnit.setAttachment(attachment); - return attachmentUnit; - } - else if (importedLectureUnit instanceof OnlineUnit importedOnlineUnit) { - OnlineUnit onlineUnit = new OnlineUnit(); - onlineUnit.setName(importedOnlineUnit.getName()); - onlineUnit.setReleaseDate(importedOnlineUnit.getReleaseDate()); - onlineUnit.setDescription(importedOnlineUnit.getDescription()); - onlineUnit.setSource(importedOnlineUnit.getSource()); - - return onlineUnit; - } - else if (importedLectureUnit instanceof ExerciseUnit) { - // TODO: Import exercises and link them to the exerciseUnit - // We have a dedicated exercise import system, so this is left out for now - return null; - } - return null; - } + channelService.createLectureChannel(savedLecture, Optional.empty()); - /** - * This helper function clones the {@code importedAttachment} (and duplicates its file) and returns it - * - * @param entityId The id of the new entity to which the attachment is linked - * @param importedAttachment The original attachment to be copied - * @return The cloned attachment with the file also duplicated to the temp directory on disk - */ - private Attachment cloneAttachment(Long entityId, final Attachment importedAttachment) { - log.debug("Creating a new Attachment from attachment {}", importedAttachment); - - Attachment attachment = new Attachment(); - attachment.setName(importedAttachment.getName()); - attachment.setUploadDate(importedAttachment.getUploadDate()); - attachment.setReleaseDate(importedAttachment.getReleaseDate()); - attachment.setVersion(importedAttachment.getVersion()); - attachment.setAttachmentType(importedAttachment.getAttachmentType()); - - Path oldPath = FilePathService.actualPathForPublicPathOrThrow(URI.create(importedAttachment.getLink())); - Path newPath; - if (oldPath.toString().contains("/attachment-unit/")) { - newPath = FilePathService.getAttachmentUnitFilePath().resolve(entityId.toString()); - } - else { - newPath = FilePathService.getLectureAttachmentFilePath().resolve(entityId.toString()); - } - log.debug("Copying attachment file from {} to {}", oldPath, newPath); - Path savePath = fileService.copyExistingFileToTarget(oldPath, newPath); - attachment.setLink(FilePathService.publicPathForActualPathOrThrow(savePath, entityId).toString()); - return attachment; + return savedLecture; } } diff --git a/src/main/java/de/tum/cit/aet/artemis/lecture/service/LectureUnitImportService.java b/src/main/java/de/tum/cit/aet/artemis/lecture/service/LectureUnitImportService.java new file mode 100644 index 000000000000..0c537eda34bc --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/lecture/service/LectureUnitImportService.java @@ -0,0 +1,175 @@ +package de.tum.cit.aet.artemis.lecture.service; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + +import java.net.URI; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import de.tum.cit.aet.artemis.core.service.FilePathService; +import de.tum.cit.aet.artemis.core.service.FileService; +import de.tum.cit.aet.artemis.iris.repository.IrisSettingsRepository; +import de.tum.cit.aet.artemis.iris.service.pyris.PyrisWebhookService; +import de.tum.cit.aet.artemis.lecture.domain.Attachment; +import de.tum.cit.aet.artemis.lecture.domain.AttachmentUnit; +import de.tum.cit.aet.artemis.lecture.domain.ExerciseUnit; +import de.tum.cit.aet.artemis.lecture.domain.Lecture; +import de.tum.cit.aet.artemis.lecture.domain.LectureUnit; +import de.tum.cit.aet.artemis.lecture.domain.OnlineUnit; +import de.tum.cit.aet.artemis.lecture.domain.TextUnit; +import de.tum.cit.aet.artemis.lecture.domain.VideoUnit; +import de.tum.cit.aet.artemis.lecture.repository.AttachmentRepository; +import de.tum.cit.aet.artemis.lecture.repository.LectureUnitRepository; + +@Profile(PROFILE_CORE) +@Service +public class LectureUnitImportService { + + private static final Logger log = LoggerFactory.getLogger(LectureUnitImportService.class); + + private final LectureUnitRepository lectureUnitRepository; + + private final AttachmentRepository attachmentRepository; + + private final FileService fileService; + + private final SlideSplitterService slideSplitterService; + + private final Optional pyrisWebhookService; + + private final Optional irisSettingsRepository; + + public LectureUnitImportService(LectureUnitRepository lectureUnitRepository, AttachmentRepository attachmentRepository, FileService fileService, + SlideSplitterService slideSplitterService, Optional pyrisWebhookService, Optional irisSettingsRepository) { + this.lectureUnitRepository = lectureUnitRepository; + this.attachmentRepository = attachmentRepository; + this.fileService = fileService; + this.slideSplitterService = slideSplitterService; + this.pyrisWebhookService = pyrisWebhookService; + this.irisSettingsRepository = irisSettingsRepository; + } + + /** + * This function imports the lecture units from the {@code importedLecture} and appends them to the {@code lecture} + * + * @param importedLecture The original lecture to be copied + * @param lecture The new lecture to which the lecture units are appended + */ + public void importLectureUnits(Lecture importedLecture, Lecture lecture) { + log.debug("Importing lecture units from lecture with Id {}", importedLecture.getId()); + List lectureUnits = new ArrayList<>(); + for (LectureUnit lectureUnit : importedLecture.getLectureUnits()) { + LectureUnit clonedLectureUnit = importLectureUnit(lectureUnit); + if (clonedLectureUnit != null) { + clonedLectureUnit.setLecture(lecture); + lectureUnits.add(clonedLectureUnit); + } + } + lecture.setLectureUnits(lectureUnits); + lectureUnitRepository.saveAll(lectureUnits); + + // Send lectures to pyris + if (pyrisWebhookService.isPresent() && irisSettingsRepository.isPresent()) { + pyrisWebhookService.get().autoUpdateAttachmentUnitsInPyris(lecture.getCourse().getId(), + lectureUnits.stream().filter(lectureUnit -> lectureUnit instanceof AttachmentUnit).map(lectureUnit -> (AttachmentUnit) lectureUnit).toList()); + } + } + + /** + * This function imports the {@code importedLectureUnit} and returns it + * + * @param importedLectureUnit The original lecture unit to be copied + * @return The imported lecture unit + */ + public LectureUnit importLectureUnit(final LectureUnit importedLectureUnit) { + log.debug("Creating a new LectureUnit from lecture unit {}", importedLectureUnit); + + switch (importedLectureUnit) { + case TextUnit importedTextUnit -> { + TextUnit textUnit = new TextUnit(); + textUnit.setName(importedTextUnit.getName()); + textUnit.setReleaseDate(importedTextUnit.getReleaseDate()); + textUnit.setContent(importedTextUnit.getContent()); + + return lectureUnitRepository.save(textUnit); + } + case VideoUnit importedVideoUnit -> { + VideoUnit videoUnit = new VideoUnit(); + videoUnit.setName(importedVideoUnit.getName()); + videoUnit.setReleaseDate(importedVideoUnit.getReleaseDate()); + videoUnit.setDescription(importedVideoUnit.getDescription()); + videoUnit.setSource(importedVideoUnit.getSource()); + + return lectureUnitRepository.save(videoUnit); + } + case AttachmentUnit importedAttachmentUnit -> { + // Create and save the attachment unit, then the attachment itself, as the id is needed for file handling + AttachmentUnit attachmentUnit = new AttachmentUnit(); + attachmentUnit.setDescription(importedAttachmentUnit.getDescription()); + attachmentUnit = lectureUnitRepository.save(attachmentUnit); + + Attachment attachment = importAttachment(attachmentUnit.getId(), importedAttachmentUnit.getAttachment()); + attachment.setAttachmentUnit(attachmentUnit); + attachmentRepository.save(attachment); + if (attachment.getLink().endsWith(".pdf")) { + slideSplitterService.splitAttachmentUnitIntoSingleSlides(attachmentUnit); + } + attachmentUnit.setAttachment(attachment); + return attachmentUnit; + } + case OnlineUnit importedOnlineUnit -> { + OnlineUnit onlineUnit = new OnlineUnit(); + onlineUnit.setName(importedOnlineUnit.getName()); + onlineUnit.setReleaseDate(importedOnlineUnit.getReleaseDate()); + onlineUnit.setDescription(importedOnlineUnit.getDescription()); + onlineUnit.setSource(importedOnlineUnit.getSource()); + + return lectureUnitRepository.save(onlineUnit); + } + case ExerciseUnit ignored -> { + // TODO: Import exercises and link them to the exerciseUnit + // We have a dedicated exercise import system, so this is left out for now + return null; + } + default -> throw new IllegalArgumentException("Unknown lecture unit type: " + importedLectureUnit.getClass()); + } + } + + /** + * This function imports the {@code importedAttachment}, and duplicates its file and returns it + * + * @param entityId The id of the new entity to which the attachment is linked + * @param importedAttachment The original attachment to be copied + * @return The imported attachment with the file also duplicated to the temp directory on disk + */ + public Attachment importAttachment(Long entityId, final Attachment importedAttachment) { + log.debug("Creating a new Attachment from attachment {}", importedAttachment); + + Attachment attachment = new Attachment(); + attachment.setName(importedAttachment.getName()); + attachment.setUploadDate(importedAttachment.getUploadDate()); + attachment.setReleaseDate(importedAttachment.getReleaseDate()); + attachment.setVersion(importedAttachment.getVersion()); + attachment.setAttachmentType(importedAttachment.getAttachmentType()); + + Path oldPath = FilePathService.actualPathForPublicPathOrThrow(URI.create(importedAttachment.getLink())); + Path newPath; + if (oldPath.toString().contains("/attachment-unit/")) { + newPath = FilePathService.getAttachmentUnitFilePath().resolve(entityId.toString()); + } + else { + newPath = FilePathService.getLectureAttachmentFilePath().resolve(entityId.toString()); + } + log.debug("Copying attachment file from {} to {}", oldPath, newPath); + Path savePath = fileService.copyExistingFileToTarget(oldPath, newPath); + attachment.setLink(FilePathService.publicPathForActualPathOrThrow(savePath, entityId).toString()); + return attachment; + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/lecture/web/LectureResource.java b/src/main/java/de/tum/cit/aet/artemis/lecture/web/LectureResource.java index a69bc110de6f..54ae76bf3bad 100644 --- a/src/main/java/de/tum/cit/aet/artemis/lecture/web/LectureResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/lecture/web/LectureResource.java @@ -255,8 +255,7 @@ public ResponseEntity importLecture(@PathVariable long sourceLectureId, authCheckService.checkHasAtLeastRoleForLectureElseThrow(Role.EDITOR, sourceLecture, user); authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, destinationCourse, user); - final var savedLecture = lectureImportService.importLecture(sourceLecture, destinationCourse); - channelService.createLectureChannel(savedLecture, Optional.empty()); + final var savedLecture = lectureImportService.importLecture(sourceLecture, destinationCourse, true); return ResponseEntity.created(new URI("/api/lectures/" + savedLecture.getId())).body(savedLecture); } diff --git a/src/main/java/de/tum/cit/aet/artemis/lti/web/admin/AdminLtiConfigurationResource.java b/src/main/java/de/tum/cit/aet/artemis/lti/web/admin/AdminLtiConfigurationResource.java index 654facd44302..0b5d3f71c2f3 100644 --- a/src/main/java/de/tum/cit/aet/artemis/lti/web/admin/AdminLtiConfigurationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/lti/web/admin/AdminLtiConfigurationResource.java @@ -33,6 +33,7 @@ * Handles administrative actions for LTI platforms, including configuration, deletion, and dynamic registration. */ @RestController +@EnforceAdmin @RequestMapping("api/admin/") @Profile(PROFILE_LTI) public class AdminLtiConfigurationResource { @@ -75,7 +76,6 @@ public AdminLtiConfigurationResource(LtiPlatformConfigurationRepository ltiPlatf * @return a {@code ResponseEntity} with an {@code Optional} and HTTP status. */ @GetMapping("lti-platform/{platformId}") - @EnforceAdmin public ResponseEntity getLtiPlatformConfiguration(@PathVariable("platformId") String platformId) { log.debug("REST request to configured lti platform"); LtiPlatformConfiguration platform = ltiPlatformConfigurationRepository.findByIdElseThrow(Long.parseLong(platformId)); @@ -89,7 +89,6 @@ public ResponseEntity getLtiPlatformConfiguration(@Pat * @return a {@code ResponseEntity} with status {@code 200 (OK)} and a header indicating the deletion. */ @DeleteMapping("lti-platform/{platformId}") - @EnforceAdmin public ResponseEntity deleteLtiPlatformConfiguration(@PathVariable("platformId") String platformId) { log.debug("REST request to configured lti platform"); LtiPlatformConfiguration platform = ltiPlatformConfigurationRepository.findByIdElseThrow(Long.parseLong(platformId)); @@ -105,7 +104,6 @@ public ResponseEntity deleteLtiPlatformConfiguration(@PathVariable("platfo * or with status 400 (Bad Request) if the provided platform configuration is invalid (e.g., missing ID) */ @PutMapping("lti-platform") - @EnforceAdmin public ResponseEntity updateLtiPlatformConfiguration(@RequestBody LtiPlatformConfiguration platform) { log.debug("REST request to update configured lti platform"); @@ -125,7 +123,6 @@ public ResponseEntity updateLtiPlatformConfiguration(@RequestBody LtiPlatf * or with status 400 (Bad Request) if the provided platform configuration is invalid (e.g., missing ID) */ @PostMapping("lti-platform") - @EnforceAdmin public ResponseEntity addLtiPlatformConfiguration(@RequestBody LtiPlatformConfiguration platform) { log.debug("REST request to add new lti platform"); @@ -147,7 +144,6 @@ public ResponseEntity addLtiPlatformConfiguration(@RequestBody LtiPlatform * @return a {@link ResponseEntity} with status 200 (OK) if the dynamic registration process was successful. */ @PostMapping("lti13/dynamic-registration") - @EnforceAdmin public ResponseEntity lti13DynamicRegistration(@RequestParam(name = "openid_configuration") String openIdConfiguration, @RequestParam(name = "registration_token", required = false) String registrationToken) { diff --git a/src/main/java/de/tum/cit/aet/artemis/modeling/repository/ModelingExerciseRepository.java b/src/main/java/de/tum/cit/aet/artemis/modeling/repository/ModelingExerciseRepository.java index f6e80c0c17b2..626bb3c86694 100644 --- a/src/main/java/de/tum/cit/aet/artemis/modeling/repository/ModelingExerciseRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/modeling/repository/ModelingExerciseRepository.java @@ -6,6 +6,7 @@ import java.time.ZonedDateTime; import java.util.List; import java.util.Optional; +import java.util.Set; import jakarta.validation.constraints.NotNull; @@ -16,6 +17,7 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import de.tum.cit.aet.artemis.core.exception.NoUniqueQueryException; import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; import de.tum.cit.aet.artemis.modeling.domain.ModelingExercise; @@ -54,9 +56,10 @@ public interface ModelingExerciseRepository extends ArtemisJpaRepository findByIdWithExampleSubmissionsAndResultsAndPlagiarismDetectionConfig(@Param("exerciseId") Long exerciseId); + Optional findByIdWithExampleSubmissionsAndResultsAndPlagiarismDetectionConfigAndGradingCriteria(@Param("exerciseId") Long exerciseId); /** * Get all modeling exercises that need to be scheduled: Those must satisfy one of the following requirements: @@ -94,6 +97,31 @@ public interface ModelingExerciseRepository extends ArtemisJpaRepository findWithStudentParticipationsSubmissionsResultsById(Long exerciseId); + @Query(""" + SELECT m + FROM ModelingExercise m + LEFT JOIN FETCH m.competencies + WHERE m.title = :title + AND m.course.id = :courseId + """) + Set findAllWithCompetenciesByTitleAndCourseId(@Param("title") String title, @Param("courseId") long courseId); + + /** + * Finds a modeling exercise by its title and course id and throws a NoUniqueQueryException if multiple exercises are found. + * + * @param title the title of the exercise + * @param courseId the id of the course + * @return the exercise with the given title and course id + * @throws NoUniqueQueryException if multiple exercises are found with the same title + */ + default Optional findUniqueWithCompetenciesByTitleAndCourseId(String title, long courseId) throws NoUniqueQueryException { + Set allExercises = findAllWithCompetenciesByTitleAndCourseId(title, courseId); + if (allExercises.size() > 1) { + throw new NoUniqueQueryException("Found multiple exercises with title " + title + " in course with id " + courseId); + } + return allExercises.stream().findFirst(); + } + @NotNull default ModelingExercise findWithEagerExampleSubmissionsAndCompetenciesByIdElseThrow(long exerciseId) { return getValueElseThrow(findWithEagerExampleSubmissionsAndCompetenciesById(exerciseId), exerciseId); @@ -106,7 +134,7 @@ default ModelingExercise findWithEagerExampleSubmissionsAndCompetenciesAndPlagia @NotNull default ModelingExercise findByIdWithExampleSubmissionsAndResultsAndPlagiarismDetectionConfigElseThrow(long exerciseId) { - return getValueElseThrow(findByIdWithExampleSubmissionsAndResultsAndPlagiarismDetectionConfig(exerciseId), exerciseId); + return getValueElseThrow(findByIdWithExampleSubmissionsAndResultsAndPlagiarismDetectionConfigAndGradingCriteria(exerciseId), exerciseId); } @NotNull diff --git a/src/main/java/de/tum/cit/aet/artemis/modeling/service/ModelingSubmissionService.java b/src/main/java/de/tum/cit/aet/artemis/modeling/service/ModelingSubmissionService.java index 320d041325e9..c8b4285de3b9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/modeling/service/ModelingSubmissionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/modeling/service/ModelingSubmissionService.java @@ -131,12 +131,10 @@ public ModelingSubmission handleModelingSubmission(ModelingSubmission modelingSu } // if athena results are present, then create a new submission on submit - if (modelingSubmission.getParticipation() != null && modelingSubmission.getParticipation().getResults() != null - && !modelingSubmission.getParticipation().getResults().isEmpty()) { - log.debug("Creating a new submission due to Athena results for user: {}", user.getLogin()); + // If results exist for this submission, create a new submission by setting the ID to null + if (modelingSubmission.getId() != null && resultRepository.existsBySubmissionId(modelingSubmission.getId())) { modelingSubmission.setId(null); } - modelingSubmission = save(modelingSubmission, exercise, user, participation); return modelingSubmission; } diff --git a/src/main/java/de/tum/cit/aet/artemis/modeling/web/ModelingSubmissionResource.java b/src/main/java/de/tum/cit/aet/artemis/modeling/web/ModelingSubmissionResource.java index e48f94b8e955..501309aea8e8 100644 --- a/src/main/java/de/tum/cit/aet/artemis/modeling/web/ModelingSubmissionResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/modeling/web/ModelingSubmissionResource.java @@ -126,7 +126,6 @@ public ResponseEntity createModelingSubmission(@PathVariable @PutMapping("exercises/{exerciseId}/modeling-submissions") @EnforceAtLeastStudent public ResponseEntity updateModelingSubmission(@PathVariable long exerciseId, @Valid @RequestBody ModelingSubmission modelingSubmission) { - log.debug("REST request to update modeling submission: {}", modelingSubmission.getModel()); if (modelingSubmission.getId() == null) { return createModelingSubmission(exerciseId, modelingSubmission); } diff --git a/src/main/java/de/tum/cit/aet/artemis/modeling/web/admin/AdminModelingExerciseResource.java b/src/main/java/de/tum/cit/aet/artemis/modeling/web/admin/AdminModelingExerciseResource.java index 215ea0a05293..cf3c6d2ee627 100644 --- a/src/main/java/de/tum/cit/aet/artemis/modeling/web/admin/AdminModelingExerciseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/modeling/web/admin/AdminModelingExerciseResource.java @@ -25,6 +25,7 @@ * REST controller for administrating ModelingExercise. */ @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminModelingExerciseResource { @@ -59,7 +60,6 @@ public AdminModelingExerciseResource(ModelingExerciseRepository modelingExercise * @return the ResponseEntity with status 200 (OK) */ @GetMapping("modeling-exercises/{exerciseId}/check-clusters") - @EnforceAdmin public ResponseEntity checkClusters(@PathVariable Long exerciseId) { log.info("REST request to check clusters of ModelingExercise : {}", exerciseId); int clusterCount = modelClusterRepository.countByExerciseIdWithEagerElements(exerciseId); @@ -73,7 +73,6 @@ public ResponseEntity checkClusters(@PathVariable Long exerciseId) { * @return the ResponseEntity with status 200 (OK) */ @DeleteMapping("modeling-exercises/{exerciseId}/clusters") - @EnforceAdmin public ResponseEntity deleteModelingExerciseClustersAndElements(@PathVariable Long exerciseId) { log.info("REST request to delete ModelingExercise : {}", exerciseId); var modelingExercise = modelingExerciseRepository.findByIdElseThrow(exerciseId); @@ -91,7 +90,6 @@ public ResponseEntity deleteModelingExerciseClustersAndElements(@PathVaria * @return the ResponseEntity with status 200 (OK) */ @PostMapping("modeling-exercises/{exerciseId}/trigger-automatic-assessment") - @EnforceAdmin public ResponseEntity triggerAutomaticAssessment(@PathVariable Long exerciseId) { instanceMessageSendService.sendModelingExerciseInstantClustering(exerciseId); return ResponseEntity.ok().build(); diff --git a/src/main/java/de/tum/cit/aet/artemis/plagiarism/service/ProgrammingPlagiarismDetectionService.java b/src/main/java/de/tum/cit/aet/artemis/plagiarism/service/ProgrammingPlagiarismDetectionService.java index a3c2cec8e9df..ea6ebfdf3b81 100644 --- a/src/main/java/de/tum/cit/aet/artemis/plagiarism/service/ProgrammingPlagiarismDetectionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/plagiarism/service/ProgrammingPlagiarismDetectionService.java @@ -32,10 +32,13 @@ import de.jplag.clustering.ClusteringOptions; import de.jplag.exceptions.ExitException; import de.jplag.java.JavaLanguage; +import de.jplag.javascript.JavaScriptLanguage; import de.jplag.kotlin.KotlinLanguage; import de.jplag.options.JPlagOptions; import de.jplag.python3.PythonLanguage; import de.jplag.reporting.reportobject.ReportObjectFactory; +import de.jplag.rlang.RLanguage; +import de.jplag.rust.RustLanguage; import de.jplag.swift.SwiftLanguage; import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; import de.tum.cit.aet.artemis.core.exception.GitException; @@ -308,13 +311,17 @@ public void deleteTempLocalRepository(Repository repository) { private Language getJPlagProgrammingLanguage(ProgrammingExercise programmingExercise) { return switch (programmingExercise.getProgrammingLanguage()) { - case JAVA -> new JavaLanguage(); case C -> new CLanguage(); + case JAVA -> new JavaLanguage(); + case JAVASCRIPT -> new JavaScriptLanguage(); + case KOTLIN -> new KotlinLanguage(); case PYTHON -> new PythonLanguage(); + case R -> new RLanguage(); + case RUST -> new RustLanguage(); case SWIFT -> new SwiftLanguage(); - case KOTLIN -> new KotlinLanguage(); - default -> throw new BadRequestAlertException("Programming language " + programmingExercise.getProgrammingLanguage() + " not supported for plagiarism check.", - "ProgrammingExercise", "notSupported"); + case EMPTY, PHP, DART, HASKELL, ASSEMBLER, OCAML, C_SHARP, C_PLUS_PLUS, SQL, TYPESCRIPT, GO, MATLAB, BASH, VHDL, RUBY, POWERSHELL, ADA -> + throw new BadRequestAlertException("Programming language " + programmingExercise.getProgrammingLanguage() + " not supported for plagiarism check.", + "ProgrammingExercise", "notSupported"); }; } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/AuthenticationMechanism.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/AuthenticationMechanism.java index 3caec801ed53..239ef3674d44 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/domain/AuthenticationMechanism.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/AuthenticationMechanism.java @@ -20,5 +20,5 @@ public enum AuthenticationMechanism { /** * The user used the artemis client code editor to authenticate to the LocalVC */ - CODE_EDITOR + CODE_EDITOR, } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExercise.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExercise.java index df7911670a22..c2a4666c7c1b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExercise.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExercise.java @@ -712,8 +712,7 @@ private boolean checkForRatedAndAssessedResult(Result result) { * @return true if the result is manual and the assessment is over, or it is an automatic result, false otherwise */ private boolean checkForAssessedResult(Result result) { - return result.getCompletionDate() != null - && ((result.isManual() && ExerciseDateService.isAfterAssessmentDueDate(this)) || result.isAutomatic() || result.isAthenaAutomatic()); + return result.getCompletionDate() != null && ((result.isManual() && ExerciseDateService.isAfterAssessmentDueDate(this)) || result.isAutomatic() || result.isAthenaBased()); } @Override diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExerciseBuildConfig.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExerciseBuildConfig.java index e8b5e554c37d..a5ada6708999 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExerciseBuildConfig.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExerciseBuildConfig.java @@ -49,7 +49,13 @@ public class ProgrammingExerciseBuildConfig extends DomainObject { private boolean checkoutSolutionRepository = false; @Column(name = "checkout_path") - private String checkoutPath; + private String testCheckoutPath; + + @Column(name = "assignment_checkout_path") + private String assignmentCheckoutPath; + + @Column(name = "solution_checkout_path") + private String solutionCheckoutPath; @Column(name = "timeout_seconds") private int timeoutSeconds; @@ -85,7 +91,9 @@ public ProgrammingExerciseBuildConfig() { public ProgrammingExerciseBuildConfig(ProgrammingExerciseBuildConfig originalBuildConfig) { this.setBranch(originalBuildConfig.getBranch()); this.setBuildPlanConfiguration(originalBuildConfig.getBuildPlanConfiguration()); - this.setCheckoutPath(originalBuildConfig.getCheckoutPath()); + this.setTestCheckoutPath(originalBuildConfig.getTestCheckoutPath()); + this.setAssignmentCheckoutPath(originalBuildConfig.getAssignmentCheckoutPath()); + this.setSolutionCheckoutPath(originalBuildConfig.getSolutionCheckoutPath()); this.setCheckoutSolutionRepository(originalBuildConfig.getCheckoutSolutionRepository()); this.setDockerFlags(originalBuildConfig.getDockerFlags()); this.setSequentialTestRuns(originalBuildConfig.hasSequentialTestRuns()); @@ -166,12 +174,12 @@ public void setCheckoutSolutionRepository(boolean checkoutSolutionRepository) { this.checkoutSolutionRepository = checkoutSolutionRepository; } - public String getCheckoutPath() { - return checkoutPath; + public String getTestCheckoutPath() { + return testCheckoutPath; } - public void setCheckoutPath(String checkoutPath) { - this.checkoutPath = checkoutPath; + public void setTestCheckoutPath(String testCheckoutPath) { + this.testCheckoutPath = testCheckoutPath; } public int getTimeoutSeconds() { @@ -268,11 +276,27 @@ public void generateAndSetBuildPlanAccessSecret() { buildPlanAccessSecret = UUID.randomUUID().toString(); } + public String getAssignmentCheckoutPath() { + return assignmentCheckoutPath; + } + + public void setAssignmentCheckoutPath(String assignmentCheckoutPath) { + this.assignmentCheckoutPath = assignmentCheckoutPath; + } + + public String getSolutionCheckoutPath() { + return solutionCheckoutPath; + } + + public void setSolutionCheckoutPath(String solutionCheckoutPath) { + this.solutionCheckoutPath = solutionCheckoutPath; + } + @Override public String toString() { return "BuildJobConfig{" + "id=" + getId() + ", sequentialTestRuns=" + sequentialTestRuns + ", branch='" + branch + '\'' + ", buildPlanConfiguration='" + buildPlanConfiguration + '\'' + ", buildScript='" + buildScript + '\'' + ", checkoutSolutionRepository=" + checkoutSolutionRepository + ", checkoutPath='" - + checkoutPath + '\'' + ", timeoutSeconds=" + timeoutSeconds + ", dockerFlags='" + dockerFlags + '\'' + ", testwiseCoverageEnabled=" + testwiseCoverageEnabled + + testCheckoutPath + '\'' + ", timeoutSeconds=" + timeoutSeconds + ", dockerFlags='" + dockerFlags + '\'' + ", testwiseCoverageEnabled=" + testwiseCoverageEnabled + ", theiaImage='" + theiaImage + '\'' + ", allowBranching=" + allowBranching + ", branchRegex='" + branchRegex + '\'' + '}'; } } 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 4206bfe15dbc..781ad04f98c6 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 @@ -38,18 +38,19 @@ public enum ProgrammingLanguage { PHP("php"); private static final Set ENABLED_LANGUAGES = Set.of( - EMPTY, - JAVA, - PYTHON, + ASSEMBLER, C, HASKELL, + JAVA, + JAVASCRIPT, KOTLIN, - VHDL, - ASSEMBLER, - SWIFT, OCAML, + PYTHON, + R, RUST, - JAVASCRIPT + SWIFT, + VHDL, + EMPTY ); // @formatter:on diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/BuildJobRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/BuildJobRepository.java index 7c341585f60f..d7a5e662c744 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/BuildJobRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/BuildJobRepository.java @@ -89,4 +89,19 @@ default BuildJob findByBuildJobIdElseThrow(String buildJobId) { return getValueElseThrow(findByBuildJobId(buildJobId)); } + /** + * Get the number of build jobs for a list of exercise ids. + * + * @param exerciseIds the list of exercise ids + * @return the number of build jobs + */ + @Query(""" + SELECT COUNT(b) + FROM BuildJob b + LEFT JOIN Result r ON b.result.id = r.id + LEFT JOIN Participation p ON r.participation.id = p.id + LEFT JOIN Exercise e ON p.exercise.id = e.id + WHERE e.id IN :exerciseIds + """) + long countBuildJobsByExerciseIds(@Param("exerciseIds") List exerciseIds); } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseRepository.java index 370c191e0ac0..579d714b18a8 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseRepository.java @@ -116,6 +116,8 @@ Optional findWithTemplateAndSolutionParticipationTeamAssign List findAllByProjectKey(String projectKey); + List findAllByCourseId(Long courseId); + @EntityGraph(type = LOAD, attributePaths = "submissionPolicy") List findWithSubmissionPolicyByProjectKey(String projectKey); @@ -226,26 +228,6 @@ default ProgrammingExercise findOneByProjectKeyOrThrow(String projectKey, boolea """) List findAllByRecentExamEndDate(@Param("endDate1") ZonedDateTime endDate1, @Param("endDate2") ZonedDateTime endDate2); - @Query(""" - SELECT DISTINCT pe - FROM ProgrammingExercise pe - LEFT JOIN FETCH pe.studentParticipations - WHERE pe.dueDate IS NOT NULL - AND :endDate1 <= pe.dueDate - AND pe.dueDate <= :endDate2 - """) - List findAllWithStudentParticipationByRecentDueDate(@Param("endDate1") ZonedDateTime endDate1, @Param("endDate2") ZonedDateTime endDate2); - - @Query(""" - SELECT DISTINCT pe - FROM ProgrammingExercise pe - LEFT JOIN FETCH pe.studentParticipations - WHERE pe.exerciseGroup IS NOT NULL - AND :endDate1 <= pe.exerciseGroup.exam.endDate - AND pe.exerciseGroup.exam.endDate <= :endDate2 - """) - List findAllWithStudentParticipationByRecentExamEndDate(@Param("endDate1") ZonedDateTime endDate1, @Param("endDate2") ZonedDateTime endDate2); - @EntityGraph(type = LOAD, attributePaths = { "studentParticipations", "studentParticipations.team", "studentParticipations.team.students" }) Optional findWithEagerStudentParticipationsById(long exerciseId); @@ -347,9 +329,34 @@ Optional findByIdWithEagerTestCasesStaticCodeAnalysisCatego LEFT JOIN FETCH p.buildConfig WHERE p.id = :exerciseId """) - Optional findByIdWithEagerBuildConfigTestCasesStaticCodeAnalysisCategoriesHintsAndTemplateAndSolutionParticipationsAndAuxRepos( + Optional findByIdWithEagerBuildConfigTestCasesStaticCodeAnalysisCategoriesHintsAndTemplateAndSolutionParticipationsAndAuxReposAndSolutionEntriesAndBuildConfig( @Param("exerciseId") long exerciseId); + default ProgrammingExercise findByIdWithEagerTestCasesStaticCodeAnalysisCategoriesHintsAndTemplateAndSolutionParticipationsAndAuxReposAndBuildConfigElseThrow(long exerciseId) + throws EntityNotFoundException { + return getValueElseThrow(findByIdWithEagerTestCasesStaticCodeAnalysisCategoriesHintsAndTemplateAndSolutionParticipationsAndAuxReposAndBuildConfig(exerciseId), exerciseId); + } + + @Query(""" + SELECT p + FROM ProgrammingExercise p + LEFT JOIN FETCH p.testCases tc + LEFT JOIN FETCH p.staticCodeAnalysisCategories + LEFT JOIN FETCH p.exerciseHints + LEFT JOIN FETCH p.templateParticipation + LEFT JOIN FETCH p.solutionParticipation + LEFT JOIN FETCH p.auxiliaryRepositories + LEFT JOIN FETCH tc.solutionEntries + LEFT JOIN FETCH p.buildConfig + LEFT JOIN FETCH p.plagiarismDetectionConfig + WHERE p.id = :exerciseId + """) + Optional findByIdForImport(@Param("exerciseId") long exerciseId); + + default ProgrammingExercise findByIdForImportElseThrow(long exerciseId) throws EntityNotFoundException { + return getValueElseThrow(findByIdForImport(exerciseId), exerciseId); + } + /** * Returns all programming exercises that have a due date after {@code now} and have tests marked with * {@link Visibility#AFTER_DUE_DATE} but no buildAndTestStudentSubmissionsAfterDueDate. @@ -537,6 +544,24 @@ SELECT COUNT (DISTINCT p) """) Optional findByIdWithGradingCriteria(@Param("exerciseId") long exerciseId); + @Query(""" + SELECT e + FROM ProgrammingExercise e + LEFT JOIN FETCH e.competencies + WHERE e.title = :title + AND e.course.id = :courseId + """) + Optional findWithCompetenciesByTitleAndCourseId(@Param("title") String title, @Param("courseId") long courseId); + + @Query(""" + SELECT e + FROM ProgrammingExercise e + LEFT JOIN FETCH e.competencies + WHERE e.shortName = :shortName + AND e.course.id = :courseId + """) + Optional findByShortNameAndCourseIdWithCompetencies(@Param("shortName") String shortName, @Param("courseId") long courseId); + default ProgrammingExercise findByIdWithGradingCriteriaElseThrow(long exerciseId) { return getValueElseThrow(findByIdWithGradingCriteria(exerciseId), exerciseId); } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseStudentParticipationRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseStudentParticipationRepository.java index 8929c407336b..3739ed8dff71 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseStudentParticipationRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseStudentParticipationRepository.java @@ -9,6 +9,8 @@ import java.util.Optional; import org.springframework.context.annotation.Profile; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -162,6 +164,21 @@ List findWithSubmissionsByExerciseIdAnd Optional findWithSubmissionsByExerciseIdAndStudentLoginAndTestRun(@Param("exerciseId") long exerciseId, @Param("username") String username, @Param("testRun") boolean testRun); + @Query(""" + SELECT participation.repositoryUri + FROM ProgrammingExerciseStudentParticipation participation + JOIN TREAT (participation.exercise AS ProgrammingExercise) pe + LEFT JOIN pe.exerciseGroup eg + LEFT JOIN eg.exam exam + WHERE participation.repositoryUri IS NOT NULL + AND ( + (pe.dueDate IS NOT NULL AND pe.dueDate BETWEEN :earliestDate AND :latestDate) + OR (eg IS NOT NULL AND exam IS NOT NULL AND exam.endDate BETWEEN :earliestDate AND :latestDate) + ) + """) + Page findRepositoryUrisByRecentDueDateOrRecentExamEndDate(@Param("earliestDate") ZonedDateTime earliestDate, @Param("latestDate") ZonedDateTime latestDate, + Pageable pageable); + @Query(""" SELECT participation FROM ProgrammingExerciseStudentParticipation participation diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/AutomaticProgrammingExerciseCleanupService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/AutomaticProgrammingExerciseCleanupService.java index e1ba117d2148..f1989c28865c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/AutomaticProgrammingExerciseCleanupService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/AutomaticProgrammingExerciseCleanupService.java @@ -3,6 +3,7 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_SCHEDULING; import static java.time.ZonedDateTime.now; +import java.net.URISyntaxException; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.HashSet; @@ -16,6 +17,8 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @@ -25,6 +28,7 @@ import de.tum.cit.aet.artemis.exercise.service.ParticipationService; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation; +import de.tum.cit.aet.artemis.programming.domain.VcsRepositoryUri; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseRepository; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseStudentParticipationRepository; @@ -44,6 +48,8 @@ public class AutomaticProgrammingExerciseCleanupService { private final GitService gitService; + private static final int STUDENT_PARTICIPATION_CLEANUP_BATCH_SIZE = 500; + @Value("${artemis.external-system-request.batch-size}") private int externalSystemRequestBatchSize; @@ -82,49 +88,68 @@ public void cleanup() { log.error("Exception occurred during cleanupBuildPlansOnContinuousIntegrationServer", ex); } try { - cleanupGitRepositoriesOnArtemisServer(); + cleanupGitWorkingCopiesOnArtemisServer(); } catch (Exception ex) { - log.error("Exception occurred during cleanupGitRepositoriesOnArtemisServer", ex); + log.error("Exception occurred during cleanupGitWorkingCopiesOnArtemisServer", ex); } } /** * cleans up old local git repositories on the Artemis server */ - public void cleanupGitRepositoriesOnArtemisServer() { + public void cleanupGitWorkingCopiesOnArtemisServer() { SecurityUtils.setAuthorizationObject(); log.info("Cleanup git repositories on Artemis server"); // we are specifically interested in exercises older than 8 weeks - var endDate2 = ZonedDateTime.now().minusWeeks(8).truncatedTo(ChronoUnit.DAYS); + var latestDate = ZonedDateTime.now().minusWeeks(8).truncatedTo(ChronoUnit.DAYS); // NOTE: for now we would like to cover more cases to also cleanup older repositories - var endDate1 = endDate2.minusYears(1).truncatedTo(ChronoUnit.DAYS); - - // Cleanup all student repos in the REPOS folder (based on the student participations) 8 weeks after the exercise due date - log.info("Search for exercises with due date from {} until {}", endDate1, endDate2); - var programmingExercises = programmingExerciseRepository.findAllWithStudentParticipationByRecentDueDate(endDate1, endDate2); - programmingExercises.addAll(programmingExerciseRepository.findAllWithStudentParticipationByRecentExamEndDate(endDate1, endDate2)); - log.info("Found {} programming exercises {} to clean {} local student repositories", programmingExercises.size(), - programmingExercises.stream().map(ProgrammingExercise::getProjectKey).collect(Collectors.joining(", ")), - programmingExercises.stream().mapToLong(programmingExercise -> programmingExercise.getStudentParticipations().size()).sum()); - for (var programmingExercise : programmingExercises) { - for (var studentParticipation : programmingExercise.getStudentParticipations()) { - var programmingExerciseParticipation = (ProgrammingExerciseStudentParticipation) studentParticipation; - gitService.deleteLocalRepository(programmingExerciseParticipation.getVcsRepositoryUri()); - } - } + var earliestDate = latestDate.minusYears(1).truncatedTo(ChronoUnit.DAYS); + + // Cleanup all student repos in the REPOS folder (based on the student participations) 8 weeks after the exercise due date or exam end date + cleanStudentParticipationsRepositories(earliestDate, latestDate); // Cleanup template, tests and solution repos in the REPOS folder 8 weeks after the course or exam is over - log.info("Search for exercises with course or exam date from {} until {}", endDate1, endDate2); - programmingExercises = programmingExerciseRepository.findAllByRecentCourseEndDate(endDate1, endDate2); - programmingExercises.addAll(programmingExerciseRepository.findAllByRecentExamEndDate(endDate1, endDate2)); + log.info("Search for exercises with course or exam date from {} until {}", earliestDate, latestDate); + var programmingExercises = programmingExerciseRepository.findAllByRecentCourseEndDate(earliestDate, latestDate); + programmingExercises.addAll(programmingExerciseRepository.findAllByRecentExamEndDate(earliestDate, latestDate)); log.info("Found {} programming exercise to clean local template, test and solution: {}", programmingExercises.size(), programmingExercises.stream().map(ProgrammingExercise::getProjectKey).collect(Collectors.joining(", "))); - for (var programmingExercise : programmingExercises) { - gitService.deleteLocalRepository(programmingExercise.getVcsTemplateRepositoryUri()); - gitService.deleteLocalRepository(programmingExercise.getVcsSolutionRepositoryUri()); - gitService.deleteLocalRepository(programmingExercise.getVcsTestRepositoryUri()); - gitService.deleteLocalProgrammingExerciseReposFolder(programmingExercise); + if (!programmingExercises.isEmpty()) { + for (var programmingExercise : programmingExercises) { + gitService.deleteLocalRepository(programmingExercise.getVcsTemplateRepositoryUri()); + gitService.deleteLocalRepository(programmingExercise.getVcsSolutionRepositoryUri()); + gitService.deleteLocalRepository(programmingExercise.getVcsTestRepositoryUri()); + gitService.deleteLocalProgrammingExerciseReposFolder(programmingExercise); + } + log.info("Finished cleaning local template, test and solution repositories"); + } + } + + private void cleanStudentParticipationsRepositories(ZonedDateTime earliestDate, ZonedDateTime latestDate) { + log.info("Search for exercises with due date from {} until {}", earliestDate, latestDate); + // Get all relevant participation ids + Pageable pageable = Pageable.ofSize(STUDENT_PARTICIPATION_CLEANUP_BATCH_SIZE); + Page uriBatch = programmingExerciseStudentParticipationRepository.findRepositoryUrisByRecentDueDateOrRecentExamEndDate(earliestDate, latestDate, pageable); + log.info("Found {} student participations to clean local student repositories in {} batches.", uriBatch.getTotalElements(), uriBatch.getTotalPages()); + if (uriBatch.getTotalElements() > 0) { + uriBatch.forEach(this::deleteLocalRepositoryByUriString); + while (!uriBatch.isLast()) { + uriBatch = programmingExerciseStudentParticipationRepository.findRepositoryUrisByRecentDueDateOrRecentExamEndDate(earliestDate, latestDate, + uriBatch.nextPageable()); + uriBatch.forEach(this::deleteLocalRepositoryByUriString); + } + log.info("Finished cleaning local student repositories"); + } + } + + private void deleteLocalRepositoryByUriString(String uri) { + try { + VcsRepositoryUri vcsRepositoryUrl = new VcsRepositoryUri(uri); + gitService.deleteLocalRepository(vcsRepositoryUrl); + } + catch (URISyntaxException e) { + log.error("Cannot create URI for repositoryUri: {}", uri, e); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/AuxiliaryRepositoryService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/AuxiliaryRepositoryService.java index 7d9a21297e75..23aa653d7301 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/AuxiliaryRepositoryService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/AuxiliaryRepositoryService.java @@ -1,5 +1,6 @@ package de.tum.cit.aet.artemis.programming.service; +import static de.tum.cit.aet.artemis.core.config.Constants.ALLOWED_CHECKOUT_DIRECTORY; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import java.util.ArrayList; @@ -7,7 +8,6 @@ import java.util.Map; import java.util.Objects; import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -27,8 +27,6 @@ public class AuxiliaryRepositoryService { private static final String AUX_REPO_ENTITY_NAME = "programmingExercise"; - private static final Pattern ALLOWED_CHECKOUT_DIRECTORY = Pattern.compile("[\\w-]+(/[\\w-]+)*$"); - private final AuxiliaryRepositoryRepository auxiliaryRepositoryRepository; public AuxiliaryRepositoryService(AuxiliaryRepositoryRepository auxiliaryRepositoryRepository) { diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/BuildScriptProviderService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/BuildScriptProviderService.java index db9e37239d9e..487c92b3c21d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/BuildScriptProviderService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/BuildScriptProviderService.java @@ -10,6 +10,7 @@ import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.context.event.ApplicationReadyEvent; @@ -18,6 +19,8 @@ import org.springframework.core.io.Resource; import org.springframework.stereotype.Service; +import de.tum.cit.aet.artemis.core.config.Constants; +import de.tum.cit.aet.artemis.core.service.ProfileService; import de.tum.cit.aet.artemis.core.service.ResourceLoaderService; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseBuildConfig; @@ -39,14 +42,17 @@ public class BuildScriptProviderService { private final Map scriptCache = new ConcurrentHashMap<>(); + private final ProfileService profileService; + /** * Constructor for BuildScriptProvider, which loads all scripts into the cache to speed up retrieval * during the runtime of the application * * @param resourceLoaderService resourceLoaderService */ - public BuildScriptProviderService(ResourceLoaderService resourceLoaderService) { + public BuildScriptProviderService(ResourceLoaderService resourceLoaderService, ProfileService profileService) { this.resourceLoaderService = resourceLoaderService; + this.profileService = profileService; } /** @@ -69,6 +75,9 @@ public void cacheOnBoot() { String uniqueKey = directory + "_" + filename; byte[] fileContent = IOUtils.toByteArray(resource.getInputStream()); String script = new String(fileContent, StandardCharsets.UTF_8); + if (!profileService.isLocalCiActive()) { + script = replacePlaceholders(script, null, null, null); + } scriptCache.put(uniqueKey, script); } catch (IOException e) { @@ -112,6 +121,9 @@ public String getScriptFor(ProgrammingLanguage programmingLanguage, Optional projectType, Boolean stati } return String.join("_", fileNameComponents) + "." + fileExtension; } + + /** + * Replaces placeholders in the given result paths with the actual paths. + * + * @param resultPaths the result paths to replace the placeholders in + * @param buildConfig the build configuration containing the actual paths + * @return the result paths with the placeholders replaced + */ + public List replaceResultPathsPlaceholders(List resultPaths, ProgrammingExerciseBuildConfig buildConfig) { + List replacedResultPaths = new ArrayList<>(); + for (String resultPath : resultPaths) { + String replacedResultPath = replacePlaceholders(resultPath, buildConfig.getAssignmentCheckoutPath(), buildConfig.getSolutionCheckoutPath(), + buildConfig.getTestCheckoutPath()); + replacedResultPaths.add(replacedResultPath); + } + return replacedResultPaths; + } + + /** + * Replaces placeholders in the given original string with the actual paths. + * + * @param originalString the original string to replace the placeholders in + * @param assignmentRepo the assignment repository name + * @param solutionRepo the solution repository name + * @param testRepo the test repository name + * @return the original string with the placeholders replaced + */ + public String replacePlaceholders(String originalString, String assignmentRepo, String solutionRepo, String testRepo) { + assignmentRepo = !StringUtils.isBlank(assignmentRepo) ? assignmentRepo : Constants.ASSIGNMENT_REPO_NAME; + solutionRepo = solutionRepo != null && !solutionRepo.isBlank() ? solutionRepo : Constants.SOLUTION_REPO_NAME; + testRepo = testRepo != null && !testRepo.isBlank() ? testRepo : Constants.TEST_REPO_NAME; + + String replacedResultPath = originalString.replace(Constants.ASSIGNMENT_REPO_PARENT_PLACEHOLDER, assignmentRepo); + replacedResultPath = replacedResultPath.replace(Constants.ASSIGNMENT_REPO_PLACEHOLDER, "/" + assignmentRepo + "/src"); + replacedResultPath = replacedResultPath.replace(Constants.SOLUTION_REPO_PLACEHOLDER, solutionRepo); + replacedResultPath = replacedResultPath.replace(Constants.TEST_REPO_PLACEHOLDER, testRepo); + return replacedResultPath; + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java index 8c92446d22d0..935a3412b10e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java @@ -4,6 +4,7 @@ import static java.time.ZonedDateTime.now; import java.time.ZonedDateTime; +import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -11,6 +12,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -59,6 +61,9 @@ public class ProgrammingExerciseCodeReviewFeedbackService { private final ProgrammingMessagingService programmingMessagingService; + @Value("${artemis.athena.allowed-feedback-attempts:20}") + private int allowedFeedbackAttempts; + public ProgrammingExerciseCodeReviewFeedbackService(GroupNotificationService groupNotificationService, Optional athenaFeedbackSuggestionsService, SubmissionService submissionService, ResultService resultService, ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, ResultRepository resultRepository, @@ -111,14 +116,14 @@ public void generateAutomaticNonGradedFeedback(ProgrammingExerciseStudentPartici var submissionOptional = programmingExerciseParticipationService.findProgrammingExerciseParticipationWithLatestSubmissionAndResult(participation.getId()) .findLatestSubmission(); if (submissionOptional.isEmpty()) { - throw new BadRequestAlertException("No legal submissions found", "submission", "noSubmission"); + throw new BadRequestAlertException("No legal submissions found", "submission", "noSubmissionExists"); } var submission = submissionOptional.get(); // save result and transmit it over websockets to notify the client about the status var automaticResult = this.submissionService.saveNewEmptyResult(submission); automaticResult.setAssessmentType(AssessmentType.AUTOMATIC_ATHENA); - automaticResult.setRated(false); + automaticResult.setRated(true); // we want to use this feedback to give the grade in the future automaticResult.setScore(100.0); automaticResult.setSuccessful(null); automaticResult.setCompletionDate(ZonedDateTime.now().plusMinutes(5)); // we do not want to show dates without a completion date, but we want the students to know their @@ -127,7 +132,6 @@ public void generateAutomaticNonGradedFeedback(ProgrammingExerciseStudentPartici try { - setIndividualDueDateAndLockRepository(participation, programmingExercise, false); this.programmingMessagingService.notifyUserAboutNewResult(automaticResult, participation); // now the client should be able to see new result @@ -158,9 +162,10 @@ public void generateAutomaticNonGradedFeedback(ProgrammingExerciseStudentPartici feedback.setDetailText(individualFeedbackItem.description()); feedback.setHasLongFeedbackText(false); feedback.setType(FeedbackType.AUTOMATIC); - feedback.setCredits(0.0); + feedback.setCredits(individualFeedbackItem.credits()); return feedback; - }).toList(); + }).sorted(Comparator.comparing(Feedback::getCredits, Comparator.nullsLast(Comparator.naturalOrder()))).toList(); + ; automaticResult.setSuccessful(true); automaticResult.setCompletionDate(ZonedDateTime.now()); @@ -176,9 +181,6 @@ public void generateAutomaticNonGradedFeedback(ProgrammingExerciseStudentPartici this.resultRepository.save(automaticResult); this.programmingMessagingService.notifyUserAboutNewResult(automaticResult, participation); } - finally { - unlockRepository(participation, programmingExercise); - } } /** @@ -225,15 +227,10 @@ private void checkRateLimitOrThrow(ProgrammingExerciseStudentParticipation parti List athenaResults = participation.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA).toList(); - long countOfAthenaResultsInProcessOrSuccessful = athenaResults.stream().filter(result -> result.isSuccessful() == null || result.isSuccessful() == Boolean.TRUE).count(); - long countOfSuccessfulRequests = athenaResults.stream().filter(result -> result.isSuccessful() == Boolean.TRUE).count(); - if (countOfAthenaResultsInProcessOrSuccessful >= 3) { - throw new BadRequestAlertException("Cannot send additional AI feedback requests now. Try again later!", "participation", "preconditions not met"); - } - if (countOfSuccessfulRequests >= 20) { - throw new BadRequestAlertException("Maximum number of AI feedback requests reached.", "participation", "preconditions not met"); + if (countOfSuccessfulRequests >= this.allowedFeedbackAttempts) { + throw new BadRequestAlertException("Maximum number of AI feedback requests reached.", "participation", "maxAthenaResultsReached", true); } } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseRepositoryService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseRepositoryService.java index db7803c21c22..c302c5f147e5 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseRepositoryService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseRepositoryService.java @@ -17,6 +17,7 @@ import java.util.Optional; import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; import org.eclipse.jgit.api.errors.GitAPIException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -662,8 +663,32 @@ void replacePlaceholders(final ProgrammingExercise programmingExercise, final Re replacements.put("${exerciseNamePomXml}", programmingExercise.getTitle().replace(" ", "-")); // Used e.g. in artifactId replacements.put("${exerciseName}", programmingExercise.getTitle()); - replacements.put("${studentWorkingDirectory}", Constants.STUDENT_WORKING_DIRECTORY); replacements.put("${packaging}", programmingExercise.getBuildConfig().hasSequentialTestRuns() ? "pom" : "jar"); + + var buildConfig = programmingExercise.getBuildConfig(); + + // replace checkout directory placeholders + String studentWorkingDirectory = !StringUtils.isBlank(buildConfig.getAssignmentCheckoutPath()) ? buildConfig.getAssignmentCheckoutPath() : Constants.ASSIGNMENT_REPO_NAME; + if (studentWorkingDirectory.startsWith("/")) { + studentWorkingDirectory = studentWorkingDirectory.substring(1); + } + String testWorkingDirectory = buildConfig.getTestCheckoutPath() != null && !buildConfig.getTestCheckoutPath().isBlank() ? buildConfig.getTestCheckoutPath() + : Constants.TEST_REPO_NAME; + String solutionWorkingDirectory = buildConfig.getSolutionCheckoutPath() != null && !buildConfig.getSolutionCheckoutPath().isBlank() ? buildConfig.getSolutionCheckoutPath() + : Constants.SOLUTION_REPO_NAME; + + if (programmingLanguage == ProgrammingLanguage.PYTHON) { + replacements.put(Constants.ASSIGNMENT_REPO_PARENT_PLACEHOLDER, studentWorkingDirectory.replace("/", ".")); + } + else { + replacements.put(Constants.ASSIGNMENT_REPO_PARENT_PLACEHOLDER, studentWorkingDirectory); + } + replacements.put(Constants.ASSIGNMENT_REPO_PLACEHOLDER, "/" + studentWorkingDirectory + "/src"); + replacements.put(Constants.TEST_REPO_PLACEHOLDER, testWorkingDirectory); + replacements.put(Constants.SOLUTION_REPO_PLACEHOLDER, solutionWorkingDirectory); + if ((programmingLanguage == ProgrammingLanguage.JAVA && programmingExercise.getProjectType().isGradle()) || programmingLanguage == ProgrammingLanguage.RUST) { + replacements.put(Constants.ASSIGNMENT_REPO_PLACEHOLDER_NO_SLASH, studentWorkingDirectory + "/src"); + } fileService.replaceVariablesInFileRecursive(repository.getLocalPath().toAbsolutePath(), replacements, List.of("gradle-wrapper.jar")); } 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 00e854f1d218..0cbf73da21ff 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 @@ -1,5 +1,6 @@ package de.tum.cit.aet.artemis.programming.service; +import static de.tum.cit.aet.artemis.core.config.Constants.ALLOWED_CHECKOUT_DIRECTORY; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import static de.tum.cit.aet.artemis.programming.domain.build.BuildPlanType.SOLUTION; import static de.tum.cit.aet.artemis.programming.domain.build.BuildPlanType.TEMPLATE; @@ -362,6 +363,7 @@ public void validateNewProgrammingExerciseSettings(ProgrammingExercise programmi programmingExercise.validateGeneralSettings(); programmingExercise.validateProgrammingSettings(); programmingExercise.validateSettingsForFeedbackRequest(); + validateCustomCheckoutPaths(programmingExercise); auxiliaryRepositoryService.validateAndAddAuxiliaryRepositoriesOfProgrammingExercise(programmingExercise, programmingExercise.getAuxiliaryRepositories()); submissionPolicyService.validateSubmissionPolicyCreation(programmingExercise); @@ -427,6 +429,27 @@ private void validatePackageName(ProgrammingExercise programmingExercise, Progra } } + private void validateCustomCheckoutPaths(ProgrammingExercise programmingExercise) { + var buildConfig = programmingExercise.getBuildConfig(); + + boolean assignmentCheckoutPathIsValid = isValidCheckoutPath(buildConfig.getAssignmentCheckoutPath()); + boolean solutionCheckoutPathIsValid = isValidCheckoutPath(buildConfig.getSolutionCheckoutPath()); + boolean testCheckoutPathIsValid = isValidCheckoutPath(buildConfig.getTestCheckoutPath()); + + if (!assignmentCheckoutPathIsValid || !solutionCheckoutPathIsValid || !testCheckoutPathIsValid) { + throw new BadRequestAlertException("The custom checkout paths are invalid", "Exercise", "checkoutDirectoriesInvalid"); + } + } + + private boolean isValidCheckoutPath(String checkoutPath) { + // Checkout paths are optional for the assignment, solution, and test repositories. If not set, the default path is used. + if (checkoutPath == null) { + return true; + } + Matcher matcher = ALLOWED_CHECKOUT_DIRECTORY.matcher(checkoutPath); + return matcher.matches(); + } + /** * Validates static code analysis settings * @@ -438,6 +461,22 @@ public void validateStaticCodeAnalysisSettings(ProgrammingExercise programmingEx programmingExercise.validateStaticCodeAnalysisSettings(programmingLanguageFeature); } + /** + * Validates the settings of an updated programming exercise. Checks if the custom checkout paths have changed. + * + * @param originalProgrammingExercise The original programming exercise + * @param updatedProgrammingExercise The updated programming exercise + */ + public void validateCheckoutDirectoriesUnchanged(ProgrammingExercise originalProgrammingExercise, ProgrammingExercise updatedProgrammingExercise) { + var originalBuildConfig = originalProgrammingExercise.getBuildConfig(); + var updatedBuildConfig = updatedProgrammingExercise.getBuildConfig(); + if (!Objects.equals(originalBuildConfig.getAssignmentCheckoutPath(), updatedBuildConfig.getAssignmentCheckoutPath()) + || !Objects.equals(originalBuildConfig.getSolutionCheckoutPath(), updatedBuildConfig.getSolutionCheckoutPath()) + || !Objects.equals(originalBuildConfig.getTestCheckoutPath(), updatedBuildConfig.getTestCheckoutPath())) { + throw new BadRequestAlertException("The custom checkout paths cannot be changed!", "programmingExercise", "checkoutDirectoriesChanged"); + } + } + /** * Creates build plans for a new programming exercise. * 1. Create the project for the exercise on the CI Server diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/RepositoryService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/RepositoryService.java index ed5dd6cbae45..9833a9ae040b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/RepositoryService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/RepositoryService.java @@ -250,27 +250,6 @@ public byte[] getFile(Repository repository, String filename) throws IOException return fileInBytes; } - /** - * Get the mimetype of a single file from the repository - * - * @param repository in which the requested file is located. - * @param filename of the file to be probed. - * @return The mimetype of the file if found or throw an exception. - * @throws IOException if the file can't be found, is corrupt, etc. - */ - public String getFileType(Repository repository, String filename) throws IOException { - Optional file = gitService.getFileByName(repository, filename); - if (file.isEmpty()) { - throw new FileNotFoundException(); - } - String type = Files.probeContentType(file.get().toPath()); - // fallback to text/plain in case content type can not be determined - if (type == null) { - return "text/plain"; - } - return type; - } - /** * Gets the files of the repository and checks whether they were changed during a student participation. * Compares the files from the students' repository with the files of the template repository. @@ -519,8 +498,6 @@ public boolean isClean(VcsRepositoryUri repositoryUri, String defaultBranch) thr public ResponseEntity getFileFromRepository(String filename, Repository repository) throws IOException { byte[] out = getFile(repository, filename); HttpHeaders responseHeaders = new HttpHeaders(); - var contentType = getFileType(repository, filename); - responseHeaders.add("Content-Type", contentType); // Prevent the file from being interpreted as HTML by the browser when opened directly: responseHeaders.setContentDisposition(ContentDisposition.builder("attachment").filename(filename).build()); return new ResponseEntity<>(out, responseHeaders, HttpStatus.OK); 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 16285e6a0695..4c73046b1ab0 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,8 +32,8 @@ 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 -> defaultRepositoryUpgradeService; - case C_SHARP, C_PLUS_PLUS, SQL, R, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> + case KOTLIN, PYTHON, C, HASKELL, VHDL, ASSEMBLER, SWIFT, OCAML, EMPTY, RUST, JAVASCRIPT, R -> defaultRepositoryUpgradeService; + case C_SHARP, C_PLUS_PLUS, SQL, TYPESCRIPT, GO, MATLAB, BASH, 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/aeolus/AeolusTemplateService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/aeolus/AeolusTemplateService.java index 14bde3cf26b1..64321ad3b61d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/aeolus/AeolusTemplateService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/aeolus/AeolusTemplateService.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import de.tum.cit.aet.artemis.core.config.ProgrammingLanguageConfiguration; +import de.tum.cit.aet.artemis.core.service.ProfileService; import de.tum.cit.aet.artemis.core.service.ResourceLoaderService; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseBuildConfig; @@ -46,13 +47,16 @@ public class AeolusTemplateService { private final BuildScriptProviderService buildScriptProviderService; + private final ProfileService profileService; + private static final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory()); public AeolusTemplateService(ProgrammingLanguageConfiguration programmingLanguageConfiguration, ResourceLoaderService resourceLoaderService, - BuildScriptProviderService buildScriptProviderService) { + BuildScriptProviderService buildScriptProviderService, ProfileService profileService) { this.programmingLanguageConfiguration = programmingLanguageConfiguration; this.resourceLoaderService = resourceLoaderService; this.buildScriptProviderService = buildScriptProviderService; + this.profileService = profileService; } /** @@ -76,6 +80,9 @@ public void cacheOnBoot() { String uniqueKey = directory + "_" + filename; byte[] fileContent = IOUtils.toByteArray(resource.getInputStream()); String script = new String(fileContent, StandardCharsets.UTF_8); + if (!profileService.isLocalCiActive()) { + script = buildScriptProviderService.replacePlaceholders(script, null, null, null); + } Windfile windfile = readWindfile(script); this.addInstanceVariablesToWindfile(windfile, ProgrammingLanguage.valueOf(directory.toUpperCase()), optionalProjectType); templateCache.put(uniqueKey, windfile); @@ -140,6 +147,9 @@ public Windfile getWindfileFor(ProgrammingLanguage programmingLanguage, Optional log.error("No windfile found for key {}", uniqueKey); return null; } + if (!profileService.isLocalCiActive()) { + scriptCache = buildScriptProviderService.replacePlaceholders(scriptCache, null, null, null); + } Windfile windfile = readWindfile(scriptCache); this.addInstanceVariablesToWindfile(windfile, programmingLanguage, projectType); templateCache.put(uniqueKey, windfile); 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 b4f67794c073..b9050501c67a 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,8 @@ 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 -> "assignment"; - case C_SHARP, C_PLUS_PLUS, SQL, R, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> + case JAVA, PYTHON, C, HASKELL, KOTLIN, VHDL, ASSEMBLER, SWIFT, OCAML, EMPTY, RUST, JAVASCRIPT, R -> "assignment"; + case C_SHARP, C_PLUS_PLUS, SQL, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + language); }; } @@ -230,9 +230,9 @@ public String forProgrammingLanguage(ProgrammingLanguage language) { @Override public String forProgrammingLanguage(ProgrammingLanguage language) { return switch (language) { - case JAVA, PYTHON, HASKELL, KOTLIN, SWIFT, EMPTY, RUST, JAVASCRIPT -> ""; + case JAVA, PYTHON, HASKELL, KOTLIN, SWIFT, EMPTY, RUST, JAVASCRIPT, R -> ""; case C, VHDL, ASSEMBLER, OCAML -> "tests"; - case C_SHARP, C_PLUS_PLUS, SQL, R, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> + case C_SHARP, C_PLUS_PLUS, SQL, TYPESCRIPT, GO, MATLAB, BASH, 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 7cd996e35cc4..0c71114e13bb 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 @@ -25,7 +25,7 @@ public class GitLabCIProgrammingLanguageFeatureService extends ProgrammingLangua public GitLabCIProgrammingLanguageFeatureService() { programmingLanguageFeatures.put(EMPTY, new ProgrammingLanguageFeature(EMPTY, false, false, false, false, false, List.of(), false, false)); programmingLanguageFeatures.put(JAVA, new ProgrammingLanguageFeature(JAVA, false, false, false, true, false, List.of(PLAIN_MAVEN, MAVEN_MAVEN), false, false)); - programmingLanguageFeatures.put(RUST, new ProgrammingLanguageFeature(RUST, false, false, false, false, false, List.of(), false, false)); - programmingLanguageFeatures.put(JAVASCRIPT, new ProgrammingLanguageFeature(JAVASCRIPT, false, false, false, false, false, List.of(), false, false)); + programmingLanguageFeatures.put(JAVASCRIPT, new ProgrammingLanguageFeature(JAVASCRIPT, false, false, true, false, false, List.of(), false, false)); + programmingLanguageFeatures.put(RUST, new ProgrammingLanguageFeature(RUST, false, false, true, false, false, List.of(), false, false)); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/hestia/CodeHintService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/hestia/CodeHintService.java index ac73fae28454..f04de8a56db9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/hestia/CodeHintService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/hestia/CodeHintService.java @@ -13,8 +13,6 @@ import org.springframework.stereotype.Service; import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; -import de.tum.cit.aet.artemis.iris.domain.session.IrisHestiaSession; -import de.tum.cit.aet.artemis.iris.service.session.IrisHestiaSessionService; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.hestia.CodeHint; import de.tum.cit.aet.artemis.programming.domain.hestia.ProgrammingExerciseSolutionEntry; @@ -29,17 +27,14 @@ public class CodeHintService { private static final Logger log = LoggerFactory.getLogger(CodeHintService.class); - private final Optional irisHestiaSessionService; - private final CodeHintRepository codeHintRepository; private final ProgrammingExerciseTaskRepository taskRepository; private final ProgrammingExerciseSolutionEntryRepository solutionEntryRepository; - public CodeHintService(Optional irisHestiaSessionService, CodeHintRepository codeHintRepository, ProgrammingExerciseTaskRepository taskRepository, + public CodeHintService(CodeHintRepository codeHintRepository, ProgrammingExerciseTaskRepository taskRepository, ProgrammingExerciseSolutionEntryRepository solutionEntryRepository) { - this.irisHestiaSessionService = irisHestiaSessionService; this.codeHintRepository = codeHintRepository; this.taskRepository = taskRepository; this.solutionEntryRepository = solutionEntryRepository; @@ -189,17 +184,4 @@ public void updateSolutionEntriesForCodeHint(CodeHint hint) { codeHintRepository.save(hint); } - - /** - * Generates a description and content for a code hint using the Iris subsystem. - * See {@link IrisHestiaSessionService#executeRequest(IrisHestiaSession)} for more information. - * - * @param codeHint The code hint to be generated - * @return The code hint with description and content - */ - public CodeHint generateDescriptionWithIris(CodeHint codeHint) { - var irisService = irisHestiaSessionService.orElseThrow(); - var session = irisService.getOrCreateSession(codeHint); - return irisService.executeRequest(session); - } } 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 94e6bf8d27fc..45a473da9148 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 @@ -7,6 +7,7 @@ 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.PYTHON; +import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.R; 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.ProjectType.FACT; @@ -33,15 +34,16 @@ public class JenkinsProgrammingLanguageFeatureService extends ProgrammingLanguag public JenkinsProgrammingLanguageFeatureService() { // Must be extended once a new programming language is added programmingLanguageFeatures.put(EMPTY, new ProgrammingLanguageFeature(EMPTY, false, false, false, false, false, List.of(), false, false)); + programmingLanguageFeatures.put(C, new ProgrammingLanguageFeature(C, false, false, true, false, false, List.of(FACT, GCC), false, false)); + programmingLanguageFeatures.put(HASKELL, new ProgrammingLanguageFeature(HASKELL, false, false, false, false, true, List.of(), false, false)); programmingLanguageFeatures.put(JAVA, new ProgrammingLanguageFeature(JAVA, true, true, true, true, false, List.of(PLAIN_GRADLE, GRADLE_GRADLE, PLAIN_MAVEN, MAVEN_MAVEN, MAVEN_BLACKBOX), true, false)); + programmingLanguageFeatures.put(JAVASCRIPT, new ProgrammingLanguageFeature(JAVASCRIPT, false, false, true, false, false, List.of(), false, false)); programmingLanguageFeatures.put(KOTLIN, new ProgrammingLanguageFeature(KOTLIN, true, false, true, true, false, List.of(), true, false)); programmingLanguageFeatures.put(PYTHON, new ProgrammingLanguageFeature(PYTHON, false, false, true, false, false, List.of(), false, false)); + programmingLanguageFeatures.put(R, new ProgrammingLanguageFeature(R, false, false, true, false, false, List.of(), false, false)); + programmingLanguageFeatures.put(RUST, new ProgrammingLanguageFeature(RUST, false, false, true, false, false, List.of(), false, false)); // Jenkins is not supporting XCODE at the moment programmingLanguageFeatures.put(SWIFT, new ProgrammingLanguageFeature(SWIFT, false, true, true, true, false, List.of(PLAIN), false, false)); - programmingLanguageFeatures.put(C, new ProgrammingLanguageFeature(C, false, false, true, false, false, List.of(FACT, GCC), false, false)); - programmingLanguageFeatures.put(HASKELL, new ProgrammingLanguageFeature(HASKELL, false, false, false, false, true, List.of(), false, false)); - programmingLanguageFeatures.put(RUST, new ProgrammingLanguageFeature(RUST, false, false, false, false, false, List.of(), false, false)); - programmingLanguageFeatures.put(JAVASCRIPT, new ProgrammingLanguageFeature(JAVASCRIPT, false, false, false, false, false, List.of(), false, 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 6e904910ca57..f900cc0f6dd1 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 @@ -184,8 +184,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 -> jenkinsBuildPlanCreator; - case VHDL, ASSEMBLER, OCAML, C_SHARP, C_PLUS_PLUS, SQL, R, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> + case JAVA, KOTLIN, PYTHON, C, HASKELL, SWIFT, EMPTY, RUST, JAVASCRIPT, R -> jenkinsBuildPlanCreator; + case VHDL, ASSEMBLER, OCAML, C_SHARP, C_PLUS_PLUS, SQL, TYPESCRIPT, GO, MATLAB, BASH, RUBY, 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/LocalCIBuildConfigurationService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIBuildConfigurationService.java index d8c03392661e..e4803619c97d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIBuildConfigurationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIBuildConfigurationService.java @@ -11,6 +11,7 @@ import de.tum.cit.aet.artemis.core.exception.LocalCIException; 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.service.BuildScriptProviderService; import de.tum.cit.aet.artemis.programming.service.aeolus.AeolusTemplateService; import de.tum.cit.aet.artemis.programming.service.aeolus.ScriptAction; import de.tum.cit.aet.artemis.programming.service.aeolus.Windfile; @@ -21,8 +22,11 @@ public class LocalCIBuildConfigurationService { private final AeolusTemplateService aeolusTemplateService; - public LocalCIBuildConfigurationService(AeolusTemplateService aeolusTemplateService) { + private final BuildScriptProviderService buildScriptProviderService; + + public LocalCIBuildConfigurationService(AeolusTemplateService aeolusTemplateService, BuildScriptProviderService buildScriptProviderService) { this.aeolusTemplateService = aeolusTemplateService; + this.buildScriptProviderService = buildScriptProviderService; } /** @@ -34,15 +38,15 @@ public LocalCIBuildConfigurationService(AeolusTemplateService aeolusTemplateServ */ public String createBuildScript(ProgrammingExercise programmingExercise) { - StringBuilder buildScript = new StringBuilder(); - buildScript.append("#!/bin/bash\n"); - buildScript.append("cd ").append(LOCALCI_WORKING_DIRECTORY).append("/testing-dir\n"); + StringBuilder buildScriptBuilder = new StringBuilder(); + buildScriptBuilder.append("#!/bin/bash\n"); + buildScriptBuilder.append("cd ").append(LOCALCI_WORKING_DIRECTORY).append("/testing-dir\n"); ProgrammingExerciseBuildConfig buildConfig = programmingExercise.getBuildConfig(); String customScript = buildConfig.getBuildScript(); // Todo: get default script if custom script is null before trying to get actions from windfile if (customScript != null) { - buildScript.append(customScript); + buildScriptBuilder.append(customScript); } else { List actions; @@ -62,16 +66,16 @@ public String createBuildScript(ProgrammingExercise programmingExercise) { actions.forEach(action -> { String workdir = action.getWorkdir(); if (workdir != null) { - buildScript.append("cd ").append(LOCALCI_WORKING_DIRECTORY).append("/testing-dir/").append(workdir).append("\n"); + buildScriptBuilder.append("cd ").append(LOCALCI_WORKING_DIRECTORY).append("/testing-dir/").append(workdir).append("\n"); } - buildScript.append(action.getScript()).append("\n"); + buildScriptBuilder.append(action.getScript()).append("\n"); if (workdir != null) { - buildScript.append("cd ").append(LOCALCI_WORKING_DIRECTORY).append("/testing-dir\n"); + buildScriptBuilder.append("cd ").append(LOCALCI_WORKING_DIRECTORY).append("/testing-dir\n"); } }); } - return buildScript.toString(); + return buildScriptProviderService.replacePlaceholders(buildScriptBuilder.toString(), programmingExercise.getBuildConfig().getAssignmentCheckoutPath(), + programmingExercise.getBuildConfig().getSolutionCheckoutPath(), programmingExercise.getBuildConfig().getTestCheckoutPath()); } - } 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 967e05604f30..bc8292d407bb 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 @@ -10,6 +10,7 @@ import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.KOTLIN; 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.RUST; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.SWIFT; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.VHDL; @@ -39,17 +40,18 @@ public class LocalCIProgrammingLanguageFeatureService extends ProgrammingLanguag public LocalCIProgrammingLanguageFeatureService() { // Must be extended once a new programming language is added programmingLanguageFeatures.put(EMPTY, new ProgrammingLanguageFeature(EMPTY, false, false, false, false, false, List.of(), false, true)); + programmingLanguageFeatures.put(ASSEMBLER, new ProgrammingLanguageFeature(ASSEMBLER, false, false, false, false, false, List.of(), false, true)); + programmingLanguageFeatures.put(C, new ProgrammingLanguageFeature(C, false, true, true, false, false, List.of(FACT, GCC), false, true)); + programmingLanguageFeatures.put(HASKELL, new ProgrammingLanguageFeature(HASKELL, true, false, false, false, true, List.of(), false, true)); programmingLanguageFeatures.put(JAVA, new ProgrammingLanguageFeature(JAVA, true, true, true, true, false, List.of(PLAIN_GRADLE, GRADLE_GRADLE, PLAIN_MAVEN, MAVEN_MAVEN), false, true)); - programmingLanguageFeatures.put(PYTHON, new ProgrammingLanguageFeature(PYTHON, false, false, true, false, false, List.of(), false, true)); - programmingLanguageFeatures.put(C, new ProgrammingLanguageFeature(C, false, true, true, false, false, List.of(FACT, GCC), false, true)); - programmingLanguageFeatures.put(ASSEMBLER, new ProgrammingLanguageFeature(ASSEMBLER, false, false, false, false, false, List.of(), false, true)); + programmingLanguageFeatures.put(JAVASCRIPT, new ProgrammingLanguageFeature(JAVASCRIPT, false, false, true, false, false, List.of(), false, true)); programmingLanguageFeatures.put(KOTLIN, new ProgrammingLanguageFeature(KOTLIN, false, false, true, true, false, List.of(), false, true)); - programmingLanguageFeatures.put(VHDL, new ProgrammingLanguageFeature(VHDL, false, false, false, false, false, List.of(), false, true)); - programmingLanguageFeatures.put(HASKELL, new ProgrammingLanguageFeature(HASKELL, true, false, false, false, true, List.of(), false, true)); programmingLanguageFeatures.put(OCAML, new ProgrammingLanguageFeature(OCAML, false, false, false, false, true, List.of(), false, true)); + programmingLanguageFeatures.put(PYTHON, new ProgrammingLanguageFeature(PYTHON, false, false, true, false, false, List.of(), false, true)); + programmingLanguageFeatures.put(R, new ProgrammingLanguageFeature(R, false, false, true, false, false, List.of(), false, true)); + programmingLanguageFeatures.put(RUST, new ProgrammingLanguageFeature(RUST, false, false, true, false, false, List.of(), false, true)); programmingLanguageFeatures.put(SWIFT, new ProgrammingLanguageFeature(SWIFT, false, false, true, true, false, List.of(PLAIN), false, true)); - programmingLanguageFeatures.put(RUST, new ProgrammingLanguageFeature(RUST, false, false, false, false, false, List.of(), false, true)); - programmingLanguageFeatures.put(JAVASCRIPT, new ProgrammingLanguageFeature(JAVASCRIPT, false, false, false, false, false, List.of(), false, true)); + programmingLanguageFeatures.put(VHDL, new ProgrammingLanguageFeature(VHDL, false, false, false, false, false, List.of(), false, true)); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIResultProcessingService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIResultProcessingService.java index 39bfac28ca0b..4507f1a4e76d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIResultProcessingService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIResultProcessingService.java @@ -113,7 +113,9 @@ public void processResult() { if (resultQueueItem == null) { return; } - log.info("Processing build job result"); + log.info("Processing build job result with id {}", resultQueueItem.buildJobQueueItem().id()); + log.debug("Build jobs waiting in queue: {}", resultQueue.size()); + log.debug("Queued build jobs: {}", resultQueue.stream().map(i -> i.buildJobQueueItem().id()).toList()); BuildJobQueueItem buildJob = resultQueueItem.buildJobQueueItem(); BuildResult buildResult = resultQueueItem.buildResult(); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCITriggerService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCITriggerService.java index 8a9aa499ea2e..5da2860ba799 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCITriggerService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCITriggerService.java @@ -42,6 +42,7 @@ import de.tum.cit.aet.artemis.programming.repository.AuxiliaryRepositoryRepository; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.cit.aet.artemis.programming.repository.SolutionProgrammingExerciseParticipationRepository; +import de.tum.cit.aet.artemis.programming.service.BuildScriptProviderService; import de.tum.cit.aet.artemis.programming.service.GitService; import de.tum.cit.aet.artemis.programming.service.ProgrammingLanguageFeature; import de.tum.cit.aet.artemis.programming.service.aeolus.AeolusResult; @@ -75,6 +76,8 @@ public class LocalCITriggerService implements ContinuousIntegrationTriggerServic private final AeolusTemplateService aeolusTemplateService; + private final BuildScriptProviderService buildScriptProviderService; + private final ProgrammingLanguageConfiguration programmingLanguageConfiguration; private final AuxiliaryRepositoryRepository auxiliaryRepositoryRepository; @@ -102,7 +105,7 @@ public LocalCITriggerService(@Qualifier("hazelcastInstance") HazelcastInstance h LocalCIProgrammingLanguageFeatureService programmingLanguageFeatureService, Optional versionControlService, SolutionProgrammingExerciseParticipationRepository solutionProgrammingExerciseParticipationRepository, LocalCIBuildConfigurationService localCIBuildConfigurationService, GitService gitService, ExerciseDateService exerciseDateService, - ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository) { + ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository, BuildScriptProviderService buildScriptProviderService) { this.hazelcastInstance = hazelcastInstance; this.aeolusTemplateService = aeolusTemplateService; this.programmingLanguageConfiguration = programmingLanguageConfiguration; @@ -114,6 +117,7 @@ public LocalCITriggerService(@Qualifier("hazelcastInstance") HazelcastInstance h this.gitService = gitService; this.programmingExerciseBuildConfigRepository = programmingExerciseBuildConfigRepository; this.exerciseDateService = exerciseDateService; + this.buildScriptProviderService = buildScriptProviderService; } @PostConstruct @@ -149,6 +153,8 @@ public void triggerBuild(ProgrammingExerciseParticipation participation, String private void triggerBuild(ProgrammingExerciseParticipation participation, String commitHashToBuild, RepositoryType triggeredByPushTo, boolean triggerAll) throws LocalCIException { + log.info("Triggering build for participation {} and commit hash {}", participation.getId(), commitHashToBuild); + // Commit hash related to the repository that will be tested String assignmentCommitHash; @@ -302,13 +308,15 @@ private BuildConfig getBuildConfig(ProgrammingExerciseParticipation participatio } List resultPaths = getTestResultPaths(windfile); + resultPaths = buildScriptProviderService.replaceResultPathsPlaceholders(resultPaths, buildConfig); // Todo: If build agent does not have access to filesystem, we need to send the build script to the build agent and execute it there. programmingExercise.setBuildConfig(buildConfig); String buildScript = localCIBuildConfigurationService.createBuildScript(programmingExercise); return new BuildConfig(buildScript, dockerImage, commitHashToBuild, assignmentCommitHash, testCommitHash, branch, programmingLanguage, projectType, - staticCodeAnalysisEnabled, sequentialTestRunsEnabled, testwiseCoverageEnabled, resultPaths); + staticCodeAnalysisEnabled, sequentialTestRunsEnabled, testwiseCoverageEnabled, resultPaths, buildConfig.getTimeoutSeconds(), + buildConfig.getAssignmentCheckoutPath(), buildConfig.getTestCheckoutPath(), buildConfig.getSolutionCheckoutPath()); } private ProgrammingExerciseBuildConfig loadBuildConfig(ProgrammingExercise programmingExercise) { diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseExportImportResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseExportImportResource.java index f1da13be2453..a55c44871619 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseExportImportResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseExportImportResource.java @@ -86,6 +86,7 @@ * REST controller for managing ProgrammingExercise. */ @Profile(PROFILE_CORE) +@FeatureToggle(Feature.ProgrammingExercises) @RestController @RequestMapping("api/") public class ProgrammingExerciseExportImportResource { @@ -184,7 +185,6 @@ private void validateStaticCodeAnalysisSettings(ProgrammingExercise programmingE */ @PostMapping("programming-exercises/import/{sourceExerciseId}") @EnforceAtLeastEditor - @FeatureToggle(Feature.ProgrammingExercises) public ResponseEntity importProgrammingExercise(@PathVariable long sourceExerciseId, @RequestBody ProgrammingExercise newExercise, @RequestParam(defaultValue = "false") boolean recreateBuildPlans, @RequestParam(defaultValue = "false") boolean updateTemplate, @RequestParam(defaultValue = "false") boolean setTestCaseVisibilityToAfterDueDate) throws JsonProcessingException { @@ -209,7 +209,8 @@ public ResponseEntity importProgrammingExercise(@PathVariab programmingExerciseRepository.validateCourseSettings(newExercise, course); final var originalProgrammingExercise = programmingExerciseRepository - .findByIdWithEagerBuildConfigTestCasesStaticCodeAnalysisCategoriesHintsAndTemplateAndSolutionParticipationsAndAuxRepos(sourceExerciseId) + .findByIdWithEagerBuildConfigTestCasesStaticCodeAnalysisCategoriesHintsAndTemplateAndSolutionParticipationsAndAuxReposAndSolutionEntriesAndBuildConfig( + sourceExerciseId) .orElseThrow(() -> new EntityNotFoundException("ProgrammingExercise", sourceExerciseId)); var consistencyErrors = consistencyCheckService.checkConsistencyOfProgrammingExercise(originalProgrammingExercise); @@ -290,7 +291,6 @@ public ResponseEntity importProgrammingExercise(@PathVariab */ @PostMapping("courses/{courseId}/programming-exercises/import-from-file") @EnforceAtLeastEditor - @FeatureToggle(Feature.ProgrammingExercises) public ResponseEntity importProgrammingExerciseFromFile(@PathVariable long courseId, @RequestPart("programmingExercise") ProgrammingExercise programmingExercise, @RequestPart("file") MultipartFile zipFile) { final var user = userRepository.getUserWithGroupsAndAuthorities(); @@ -317,7 +317,7 @@ public ResponseEntity importProgrammingExerciseFromFile(@Pa */ @GetMapping("programming-exercises/{exerciseId}/export-instructor-exercise") @EnforceAtLeastInstructor - @FeatureToggle({ Feature.ProgrammingExercises, Feature.Exports }) + @FeatureToggle(Feature.Exports) public ResponseEntity exportInstructorExercise(@PathVariable long exerciseId) throws IOException { var programmingExercise = programmingExerciseRepository.findByIdWithPlagiarismDetectionConfigTeamConfigAndBuildConfigElseThrow(exerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.INSTRUCTOR, programmingExercise, null); @@ -351,7 +351,7 @@ public ResponseEntity exportInstructorExercise(@PathVariable long exer */ @GetMapping("programming-exercises/{exerciseId}/export-instructor-repository/{repositoryType}") @EnforceAtLeastTutor - @FeatureToggle({ Feature.ProgrammingExercises, Feature.Exports }) + @FeatureToggle(Feature.Exports) public ResponseEntity exportInstructorRepository(@PathVariable long exerciseId, @PathVariable RepositoryType repositoryType) throws IOException { var programmingExercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.TEACHING_ASSISTANT, programmingExercise, null); @@ -372,7 +372,7 @@ public ResponseEntity exportInstructorRepository(@PathVariable long ex */ @GetMapping("programming-exercises/{exerciseId}/export-instructor-auxiliary-repository/{repositoryId}") @EnforceAtLeastTutor - @FeatureToggle({ Feature.ProgrammingExercises, Feature.Exports }) + @FeatureToggle(Feature.Exports) public ResponseEntity exportInstructorAuxiliaryRepository(@PathVariable long exerciseId, @PathVariable long repositoryId) throws IOException { var programmingExercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.TEACHING_ASSISTANT, programmingExercise, null); @@ -418,7 +418,7 @@ private ResponseEntity returnZipFileForRepositoryExport(Optional */ @PostMapping("programming-exercises/{exerciseId}/export-repos-by-participant-identifiers/{participantIdentifiers}") @EnforceAtLeastTutor - @FeatureToggle({ Feature.ProgrammingExercises, Feature.Exports }) + @FeatureToggle(Feature.Exports) public ResponseEntity exportSubmissionsByStudentLogins(@PathVariable long exerciseId, @PathVariable String participantIdentifiers, @RequestBody RepositoryExportOptionsDTO repositoryExportOptions) throws IOException { var programmingExercise = programmingExerciseRepository.findByIdWithStudentParticipationsAndLegalSubmissionsElseThrow(exerciseId); @@ -463,7 +463,7 @@ public ResponseEntity exportSubmissionsByStudentLogins(@PathVariable l */ @PostMapping("programming-exercises/{exerciseId}/export-repos-by-participation-ids/{participationIds}") @EnforceAtLeastTutor - @FeatureToggle({ Feature.ProgrammingExercises, Feature.Exports }) + @FeatureToggle(Feature.Exports) public ResponseEntity exportSubmissionsByParticipationIds(@PathVariable long exerciseId, @PathVariable String participationIds, @RequestBody RepositoryExportOptionsDTO repositoryExportOptions) throws IOException { var programmingExercise = programmingExerciseRepository.findByIdWithStudentParticipationsAndLegalSubmissionsElseThrow(exerciseId); @@ -522,7 +522,7 @@ private ResponseEntity provideZipForParticipations(@NotNull List exportStudentRequestedRepository(@PathVariable long exerciseId, @RequestParam() boolean includeTests) throws IOException { var programmingExercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); if (programmingExercise.isExamExercise()) { @@ -549,7 +549,7 @@ public ResponseEntity exportStudentRequestedRepository(@PathVariable l */ @GetMapping("programming-exercises/{exerciseId}/export-student-repository/{participationId}") @EnforceAtLeastStudent - @FeatureToggle({ Feature.ProgrammingExercises, Feature.Exports }) + @FeatureToggle(Feature.Exports) public ResponseEntity exportStudentRepository(@PathVariable long exerciseId, @PathVariable long participationId) throws IOException { var programmingExercise = programmingExerciseRepository.findByIdWithStudentParticipationsAndLegalSubmissionsElseThrow(exerciseId); var studentParticipation = programmingExercise.getStudentParticipations().stream().filter(p -> p.getId().equals(participationId)) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExercisePlagiarismResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExercisePlagiarismResource.java index 4388e386ab39..95e60a0ec89c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExercisePlagiarismResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExercisePlagiarismResource.java @@ -40,6 +40,7 @@ * REST controller for managing ProgrammingExercise. */ @Profile(PROFILE_CORE) +@FeatureToggle(Feature.ProgrammingExercises) @RestController @RequestMapping("api/") public class ProgrammingExercisePlagiarismResource { @@ -72,7 +73,6 @@ public ProgrammingExercisePlagiarismResource(ProgrammingExerciseRepository progr */ @GetMapping("programming-exercises/{exerciseId}/plagiarism-result") @EnforceAtLeastEditor - @FeatureToggle(Feature.ProgrammingExercises) public ResponseEntity> getPlagiarismResult(@PathVariable long exerciseId) { log.debug("REST request to get the latest plagiarism result for the programming exercise with id: {}", exerciseId); ProgrammingExercise programmingExercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); @@ -95,7 +95,7 @@ public ResponseEntity> getPlagiarismRe */ @GetMapping("programming-exercises/{exerciseId}/check-plagiarism") @EnforceAtLeastEditor - @FeatureToggle({ Feature.ProgrammingExercises, Feature.PlagiarismChecks }) + @FeatureToggle(Feature.PlagiarismChecks) public ResponseEntity> checkPlagiarism(@PathVariable long exerciseId, @RequestParam int similarityThreshold, @RequestParam int minimumScore, @RequestParam int minimumSize) throws ExitException, IOException { ProgrammingExercise programmingExercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); @@ -128,7 +128,6 @@ public ResponseEntity> checkPlagiarism */ @GetMapping(value = "programming-exercises/{exerciseId}/check-plagiarism-jplag-report") @EnforceAtLeastEditor - @FeatureToggle(Feature.ProgrammingExercises) public ResponseEntity checkPlagiarismWithJPlagReport(@PathVariable long exerciseId, @RequestParam int similarityThreshold, @RequestParam int minimumScore, @RequestParam int minimumSize) throws IOException { log.debug("REST request to check plagiarism for ProgrammingExercise with id: {}", exerciseId); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java index 7462f19512c1..2225baa81759 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java @@ -327,6 +327,9 @@ public ResponseEntity updateProgrammingExercise(@RequestBod } } + // Verify that the checkout directories have not been changed. This is required since the buildScript and result paths are determined during the creation of the exercise. + programmingExerciseService.validateCheckoutDirectoriesUnchanged(programmingExerciseBeforeUpdate, updatedProgrammingExercise); + // Verify that a theia image is provided when the online IDE is enabled if (updatedProgrammingExercise.isAllowOnlineIde() && updatedProgrammingExercise.getBuildConfig().getTheiaImage() == null) { throw new BadRequestAlertException("You need to provide a Theia image when the online IDE is enabled", ENTITY_NAME, "noTheiaImageProvided"); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/hestia/CodeHintResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/hestia/CodeHintResource.java index 297e784f1e2b..122aa11b7a17 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/hestia/CodeHintResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/hestia/CodeHintResource.java @@ -1,11 +1,9 @@ package de.tum.cit.aet.artemis.programming.web.hestia; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; -import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_IRIS; import java.util.List; import java.util.Objects; -import java.util.Optional; import java.util.Set; import org.slf4j.Logger; @@ -23,8 +21,6 @@ import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException; import de.tum.cit.aet.artemis.core.exception.ConflictException; import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInExercise.EnforceAtLeastEditorInExercise; -import de.tum.cit.aet.artemis.iris.domain.settings.IrisSubSettingsType; -import de.tum.cit.aet.artemis.iris.service.settings.IrisSettingsService; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.hestia.CodeHint; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseRepository; @@ -50,15 +46,12 @@ public class CodeHintResource { private final CodeHintService codeHintService; - private final Optional irisSettingsService; - public CodeHintResource(ProgrammingExerciseRepository programmingExerciseRepository, ProgrammingExerciseSolutionEntryRepository solutionEntryRepository, - CodeHintRepository codeHintRepository, CodeHintService codeHintService, Optional irisSettingsService) { + CodeHintRepository codeHintRepository, CodeHintService codeHintService) { this.programmingExerciseRepository = programmingExerciseRepository; this.solutionEntryRepository = solutionEntryRepository; this.codeHintRepository = codeHintRepository; this.codeHintService = codeHintService; - this.irisSettingsService = irisSettingsService; } /** @@ -98,41 +91,6 @@ public ResponseEntity> generateCodeHintsForExercise(@PathVariable return ResponseEntity.ok(codeHints); } - /** - * {@code POST programming-exercises/:exerciseId/code-hints/:codeHintId/generate-description} : Generate a description for a code hint using Iris. - * - * @param exerciseId The id of the exercise of the code hint - * @param codeHintId The id of the code hint - * @return the {@link ResponseEntity} with status {@code 200 (Ok)} and with body the updated code hint - */ - // TODO: move into some IrisResource - @Profile(PROFILE_IRIS) - @PostMapping("programming-exercises/{exerciseId}/code-hints/{codeHintId}/generate-description") - @EnforceAtLeastEditorInExercise - public ResponseEntity generateDescriptionForCodeHint(@PathVariable Long exerciseId, @PathVariable Long codeHintId) { - log.debug("REST request to generate description with Iris for CodeHint: {}", codeHintId); - - ProgrammingExercise exercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); - irisSettingsService.orElseThrow().isEnabledForElseThrow(IrisSubSettingsType.HESTIA, exercise); - - // Hints for exam exercises are not supported at the moment - if (exercise.isExamExercise()) { - throw new AccessForbiddenException("Code hints for exams are currently not supported"); - } - - var codeHint = codeHintRepository.findByIdWithSolutionEntriesElseThrow(codeHintId); - if (!Objects.equals(codeHint.getExercise().getId(), exercise.getId())) { - throw new ConflictException("The code hint does not belong to the exercise", "CodeHint", "codeHintExerciseConflict"); - } - - if (codeHint.getSolutionEntries().isEmpty()) { - throw new ConflictException("The code hint does not have any solution entries", "CodeHint", "codeHintNoSolutionEntries"); - } - - codeHint = codeHintService.generateDescriptionWithIris(codeHint); - return ResponseEntity.ok(codeHint); - } - /** * {@code DELETE programming-exercises/:exerciseId/code-hints/:codeHintId/solution-entries/:solutionEntryId} : * Removes a solution entry from a code hint. diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/localci/AeolusTemplateResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/localci/AeolusTemplateResource.java index 9919c2440364..9e5530e94813 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/localci/AeolusTemplateResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/localci/AeolusTemplateResource.java @@ -79,8 +79,8 @@ public ResponseEntity getAeolusTemplate(@PathVariable ProgrammingLanguag } /** - * GET /api/aeolus/templates/:language/:projectType : Get the aeolus template file with the given filename
    - * GET /api/aeolus/templates/:language : Get the aeolus template file with the given filename + * GET /api/aeolus/template-scripts/:language/:projectType : Get the aeolus template file with the given filename
    + * GET /api/aeolus/template-scripts/:language : Get the aeolus template file with the given filename *

    * The windfile contains the default build plan configuration for new programming exercises. * @@ -91,7 +91,7 @@ public ResponseEntity getAeolusTemplate(@PathVariable ProgrammingLanguag * @param testCoverage Whether the test coverage template should be used * @return The requested file, or 404 if the file doesn't exist */ - @GetMapping({ "templateScripts/{language}/{projectType}", "templateScripts/{language}" }) + @GetMapping({ "template-scripts/{language}/{projectType}", "template-scripts/{language}" }) @EnforceAtLeastEditor public ResponseEntity getAeolusTemplateScript(@PathVariable ProgrammingLanguage language, @PathVariable Optional projectType, @RequestParam(value = "staticAnalysis", defaultValue = "false") boolean staticAnalysis, diff --git a/src/main/java/de/tum/cit/aet/artemis/quiz/domain/DragAndDropSubmittedAnswer.java b/src/main/java/de/tum/cit/aet/artemis/quiz/domain/DragAndDropSubmittedAnswer.java index cfc17f0b42ee..7ab065bb4920 100644 --- a/src/main/java/de/tum/cit/aet/artemis/quiz/domain/DragAndDropSubmittedAnswer.java +++ b/src/main/java/de/tum/cit/aet/artemis/quiz/domain/DragAndDropSubmittedAnswer.java @@ -76,7 +76,6 @@ public DragItem getSelectedDragItemForDropLocation(DropLocation dropLocation) { * @param question the changed question with the changed DragItems and DropLocations */ private void checkAndDeleteMappings(DragAndDropQuestion question) { - if (question != null) { // Check if a dragItem or dropLocation was deleted and delete reference to it in mappings Set selectedMappingsToDelete = new HashSet<>(); @@ -98,7 +97,6 @@ private void checkAndDeleteMappings(DragAndDropQuestion question) { */ @Override public void checkAndDeleteReferences(QuizExercise quizExercise) { - // Delete all references to question, dropLocations and dragItem if the question was deleted if (!quizExercise.getQuizQuestions().contains(getQuizQuestion())) { setQuizQuestion(null); diff --git a/src/main/java/de/tum/cit/aet/artemis/quiz/domain/DragItem.java b/src/main/java/de/tum/cit/aet/artemis/quiz/domain/DragItem.java index bcf311e9e31c..e657930b29f9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/quiz/domain/DragItem.java +++ b/src/main/java/de/tum/cit/aet/artemis/quiz/domain/DragItem.java @@ -109,6 +109,10 @@ public void setInvalid(Boolean invalid) { this.invalid = invalid; } + public void setMappings(Set mappings) { + this.mappings = mappings; + } + /** * This method is called after the entity is saved for the first time. We replace the placeholder in the pictureFilePath with the id of the entity because we don't know it * before creation. diff --git a/src/main/java/de/tum/cit/aet/artemis/quiz/domain/DropLocation.java b/src/main/java/de/tum/cit/aet/artemis/quiz/domain/DropLocation.java index 8237750bf7b0..468210dc7cab 100644 --- a/src/main/java/de/tum/cit/aet/artemis/quiz/domain/DropLocation.java +++ b/src/main/java/de/tum/cit/aet/artemis/quiz/domain/DropLocation.java @@ -128,6 +128,10 @@ public void setInvalid(Boolean invalid) { this.invalid = invalid; } + public void setMappings(Set mappings) { + this.mappings = mappings; + } + /** * check if the DropLocation is solved correctly * diff --git a/src/main/java/de/tum/cit/aet/artemis/quiz/domain/ShortAnswerSolution.java b/src/main/java/de/tum/cit/aet/artemis/quiz/domain/ShortAnswerSolution.java index f55298937de9..448507563f89 100644 --- a/src/main/java/de/tum/cit/aet/artemis/quiz/domain/ShortAnswerSolution.java +++ b/src/main/java/de/tum/cit/aet/artemis/quiz/domain/ShortAnswerSolution.java @@ -77,6 +77,10 @@ public void setQuestion(ShortAnswerQuestion shortAnswerQuestion) { this.question = shortAnswerQuestion; } + public void setMappings(Set mappings) { + this.mappings = mappings; + } + @Override public String toString() { return "ShortAnswerSolution{" + "id=" + getId() + ", text='" + getText() + "'" + ", invalid='" + isInvalid() + "'" + "}"; diff --git a/src/main/java/de/tum/cit/aet/artemis/quiz/domain/ShortAnswerSpot.java b/src/main/java/de/tum/cit/aet/artemis/quiz/domain/ShortAnswerSpot.java index 2d428efa5acb..727861554b7f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/quiz/domain/ShortAnswerSpot.java +++ b/src/main/java/de/tum/cit/aet/artemis/quiz/domain/ShortAnswerSpot.java @@ -94,6 +94,10 @@ public void setQuestion(ShortAnswerQuestion shortAnswerQuestion) { this.question = shortAnswerQuestion; } + public void setMappings(Set shortAnswerMappings) { + this.mappings = shortAnswerMappings; + } + @Override public String toString() { return "ShortAnswerSpot{" + "id=" + getId() + ", width=" + getWidth() + ", spotNr=" + getSpotNr() + ", invalid='" + isInvalid() + "'" + "}"; diff --git a/src/main/java/de/tum/cit/aet/artemis/quiz/repository/QuizExerciseRepository.java b/src/main/java/de/tum/cit/aet/artemis/quiz/repository/QuizExerciseRepository.java index 5ab0a9bbb1a1..6f31cc005d1d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/quiz/repository/QuizExerciseRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/quiz/repository/QuizExerciseRepository.java @@ -6,6 +6,7 @@ import java.time.ZonedDateTime; import java.util.List; import java.util.Optional; +import java.util.Set; import jakarta.validation.constraints.NotNull; @@ -16,6 +17,7 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import de.tum.cit.aet.artemis.core.exception.NoUniqueQueryException; import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; import de.tum.cit.aet.artemis.quiz.domain.QuizExercise; @@ -60,8 +62,9 @@ public interface QuizExerciseRepository extends ArtemisJpaRepository findWithEagerQuestionsAndStatisticsById(Long quizExerciseId); - @EntityGraph(type = LOAD, attributePaths = { "quizQuestions", "quizPointStatistic", "quizQuestions.quizQuestionStatistic", "categories", "competencies", "quizBatches" }) - Optional findWithEagerQuestionsAndStatisticsAndCompetenciesById(Long quizExerciseId); + @EntityGraph(type = LOAD, attributePaths = { "quizQuestions", "quizPointStatistic", "quizQuestions.quizQuestionStatistic", "categories", "competencies", "quizBatches", + "gradingCriteria" }) + Optional findWithEagerQuestionsAndStatisticsAndCompetenciesAndBatchesAndGradingCriteriaById(Long quizExerciseId); @EntityGraph(type = LOAD, attributePaths = { "quizQuestions" }) Optional findWithEagerQuestionsById(Long quizExerciseId); @@ -72,6 +75,31 @@ public interface QuizExerciseRepository extends ArtemisJpaRepository findWithEagerBatchesById(Long quizExerciseId); + @Query(""" + SELECT q + FROM QuizExercise q + LEFT JOIN FETCH q.competencies + WHERE q.title = :title + AND q.course.id = :courseId + """) + Set findAllWithCompetenciesByTitleAndCourseId(@Param("title") String title, @Param("courseId") long courseId); + + /** + * Finds a quiz exercise by its title and course id and throws a NoUniqueQueryException if multiple exercises are found. + * + * @param title the title of the exercise + * @param courseId the id of the course + * @return the exercise with the given title and course id + * @throws NoUniqueQueryException if multiple exercises are found with the same title + */ + default Optional findUniqueWithCompetenciesByTitleAndCourseId(String title, long courseId) throws NoUniqueQueryException { + Set allExercises = findAllWithCompetenciesByTitleAndCourseId(title, courseId); + if (allExercises.size() > 1) { + throw new NoUniqueQueryException("Found multiple exercises with title " + title + " in course with id " + courseId); + } + return allExercises.stream().findFirst(); + } + @NotNull default QuizExercise findWithEagerBatchesByIdOrElseThrow(Long quizExerciseId) { return getValueElseThrow(findWithEagerBatchesById(quizExerciseId), quizExerciseId); @@ -116,7 +144,7 @@ default QuizExercise findByIdWithQuestionsAndStatisticsElseThrow(Long quizExerci } @NotNull - default QuizExercise findByIdWithQuestionsAndStatisticsAndCompetenciesElseThrow(Long quizExerciseId) { - return getValueElseThrow(findWithEagerQuestionsAndStatisticsAndCompetenciesById(quizExerciseId), quizExerciseId); + default QuizExercise findByIdWithQuestionsAndStatisticsAndCompetenciesAndBatchesAndGradingCriteriaElseThrow(Long quizExerciseId) { + return getValueElseThrow(findWithEagerQuestionsAndStatisticsAndCompetenciesAndBatchesAndGradingCriteriaById(quizExerciseId), quizExerciseId); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizExerciseImportService.java b/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizExerciseImportService.java index d6f432199a27..ffe7989c1e91 100644 --- a/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizExerciseImportService.java +++ b/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizExerciseImportService.java @@ -5,6 +5,7 @@ import java.io.IOException; import java.net.URI; import java.nio.file.Path; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -128,6 +129,7 @@ private QuizExercise copyQuizExerciseBasis(QuizExercise importedExercise) { private void copyQuizQuestions(QuizExercise sourceExercise, QuizExercise newExercise) { log.debug("Copying the QuizQuestions to new QuizExercise: {}", newExercise); + List newQuestions = new ArrayList<>(); for (QuizQuestion quizQuestion : sourceExercise.getQuizQuestions()) { quizQuestion.setId(null); quizQuestion.setQuizQuestionStatistic(null); @@ -141,15 +143,21 @@ else if (quizQuestion instanceof ShortAnswerQuestion saQuestion) { setUpShortAnswerQuestionForImport(saQuestion); } quizQuestion.setExercise(newExercise); + + newQuestions.add(quizQuestion); } - newExercise.setQuizQuestions(sourceExercise.getQuizQuestions()); + newExercise.setQuizQuestions(newQuestions); } private void setUpMultipleChoiceQuestionForImport(MultipleChoiceQuestion mcQuestion) { + List newAnswerOptions = new ArrayList<>(); for (AnswerOption answerOption : mcQuestion.getAnswerOptions()) { answerOption.setId(null); answerOption.setQuestion(mcQuestion); + + newAnswerOptions.add(answerOption); } + mcQuestion.setAnswerOptions(newAnswerOptions); } private void setUpDragAndDropQuestionForImport(DragAndDropQuestion dndQuestion) { @@ -171,19 +179,29 @@ private void setUpDragAndDropQuestionForImport(DragAndDropQuestion dndQuestion) log.warn("BackgroundFilePath of DragAndDropQuestion {} is null", dndQuestion.getId()); } + List newDropLocations = new ArrayList<>(); for (DropLocation dropLocation : dndQuestion.getDropLocations()) { dropLocation.setId(null); dropLocation.setQuestion(dndQuestion); + dropLocation.setMappings(new HashSet<>()); + + newDropLocations.add(dropLocation); } + dndQuestion.setDropLocations(newDropLocations); setUpDragItemsForImport(dndQuestion); setUpDragAndDropMappingsForImport(dndQuestion); } private void setUpDragItemsForImport(DragAndDropQuestion dndQuestion) { + List newDragItems = new ArrayList<>(); for (DragItem dragItem : dndQuestion.getDragItems()) { dragItem.setId(null); dragItem.setQuestion(dndQuestion); + dragItem.setMappings(new HashSet<>()); + + newDragItems.add(dragItem); + if (dragItem.getPictureFilePath() == null) { continue; } @@ -201,9 +219,11 @@ private void setUpDragItemsForImport(DragAndDropQuestion dndQuestion) { dragItem.setPictureFilePath(FilePathService.publicPathForActualPathOrThrow(newDragItemPath, null).toString()); } } + dndQuestion.setDragItems(newDragItems); } private void setUpDragAndDropMappingsForImport(DragAndDropQuestion dndQuestion) { + List newDragAndDropMappings = new ArrayList<>(); for (DragAndDropMapping dragAndDropMapping : dndQuestion.getCorrectMappings()) { dragAndDropMapping.setId(null); dragAndDropMapping.setQuestion(dndQuestion); @@ -213,18 +233,34 @@ private void setUpDragAndDropMappingsForImport(DragAndDropQuestion dndQuestion) if (dragAndDropMapping.getDropLocationIndex() != null) { dragAndDropMapping.setDropLocation(dndQuestion.getDropLocations().get(dragAndDropMapping.getDropLocationIndex())); } + + newDragAndDropMappings.add(dragAndDropMapping); } + dndQuestion.setCorrectMappings(newDragAndDropMappings); } private void setUpShortAnswerQuestionForImport(ShortAnswerQuestion saQuestion) { + List newShortAnswerSpots = new ArrayList<>(); for (ShortAnswerSpot shortAnswerSpot : saQuestion.getSpots()) { shortAnswerSpot.setId(null); shortAnswerSpot.setQuestion(saQuestion); + shortAnswerSpot.setMappings(new HashSet<>()); + + newShortAnswerSpots.add(shortAnswerSpot); } + saQuestion.setSpots(newShortAnswerSpots); + + List newShortAnswerSolutions = new ArrayList<>(); for (ShortAnswerSolution shortAnswerSolution : saQuestion.getSolutions()) { shortAnswerSolution.setId(null); shortAnswerSolution.setQuestion(saQuestion); + shortAnswerSolution.setMappings(new HashSet<>()); + + newShortAnswerSolutions.add(shortAnswerSolution); } + saQuestion.setSolutions(newShortAnswerSolutions); + + List newShortAnswerMappings = new ArrayList<>(); for (ShortAnswerMapping shortAnswerMapping : saQuestion.getCorrectMappings()) { shortAnswerMapping.setId(null); shortAnswerMapping.setQuestion(saQuestion); @@ -234,7 +270,9 @@ private void setUpShortAnswerQuestionForImport(ShortAnswerQuestion saQuestion) { if (shortAnswerMapping.getShortAnswerSpotIndex() != null) { shortAnswerMapping.setSpot(saQuestion.getSpots().get(shortAnswerMapping.getShortAnswerSpotIndex())); } + newShortAnswerMappings.add(shortAnswerMapping); } + saQuestion.setCorrectMappings(newShortAnswerMappings); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizExerciseWithSubmissionsExportService.java b/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizExerciseWithSubmissionsExportService.java index 207a84d3220d..0b44382a8de5 100644 --- a/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizExerciseWithSubmissionsExportService.java +++ b/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizExerciseWithSubmissionsExportService.java @@ -57,7 +57,7 @@ public QuizExerciseWithSubmissionsExportService(QuizExerciseRepository quizExerc * @return the path to the directory where the quiz exercise was exported to */ public Path exportExerciseWithSubmissions(QuizExercise quizExercise, Path exerciseExportDir, List exportErrors, List reportEntries) { - quizExercise = quizExerciseRepository.findByIdWithQuestionsAndStatisticsAndCompetenciesElseThrow(quizExercise.getId()); + quizExercise = quizExerciseRepository.findByIdWithQuestionsAndStatisticsAndCompetenciesAndBatchesAndGradingCriteriaElseThrow(quizExercise.getId()); // do not store unnecessary information in the JSON file quizExercise.setCourse(null); quizExercise.setExerciseGroup(null); diff --git a/src/main/java/de/tum/cit/aet/artemis/quiz/web/QuizExerciseResource.java b/src/main/java/de/tum/cit/aet/artemis/quiz/web/QuizExerciseResource.java index 2f055ac9bf09..118c5554a1f9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/quiz/web/QuizExerciseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/quiz/web/QuizExerciseResource.java @@ -371,7 +371,7 @@ public ResponseEntity getQuizExercise(@PathVariable Long quizExerc // TODO: Split this route in two: One for normal and one for exam exercises log.info("REST request to get quiz exercise : {}", quizExerciseId); var user = userRepository.getUserWithGroupsAndAuthorities(); - var quizExercise = quizExerciseRepository.findByIdWithQuestionsAndStatisticsAndCompetenciesElseThrow(quizExerciseId); + var quizExercise = quizExerciseRepository.findByIdWithQuestionsAndStatisticsAndCompetenciesAndBatchesAndGradingCriteriaElseThrow(quizExerciseId); if (quizExercise.isExamExercise()) { authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.EDITOR, quizExercise, user); studentParticipationRepository.checkTestRunsExist(quizExercise); diff --git a/src/main/java/de/tum/cit/aet/artemis/text/repository/TextExerciseRepository.java b/src/main/java/de/tum/cit/aet/artemis/text/repository/TextExerciseRepository.java index 6ed0ef96f4a7..3171fa825b63 100644 --- a/src/main/java/de/tum/cit/aet/artemis/text/repository/TextExerciseRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/text/repository/TextExerciseRepository.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Optional; +import java.util.Set; import jakarta.validation.constraints.NotNull; @@ -15,6 +16,7 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import de.tum.cit.aet.artemis.core.exception.NoUniqueQueryException; import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; import de.tum.cit.aet.artemis.text.domain.TextExercise; @@ -42,6 +44,20 @@ public interface TextExerciseRepository extends ArtemisJpaRepository findWithEagerTeamAssignmentConfigAndCategoriesAndCompetenciesAndPlagiarismDetectionConfigById(long exerciseId); + @Query(""" + SELECT t + FROM TextExercise t + LEFT JOIN FETCH t.exampleSubmissions e + LEFT JOIN FETCH e.submission s + LEFT JOIN FETCH s.results r + LEFT JOIN FETCH r.feedbacks + LEFT JOIN FETCH s.blocks + LEFT JOIN FETCH r.assessor + LEFT JOIN FETCH t.teamAssignmentConfig + WHERE t.id = :exerciseId + """) + Optional findWithExampleSubmissionsAndResultsById(@Param("exerciseId") long exerciseId); + @Query(""" SELECT textExercise FROM TextExercise textExercise @@ -52,9 +68,10 @@ public interface TextExerciseRepository extends ArtemisJpaRepository findWithExampleSubmissionsAndResultsById(@Param("exerciseId") long exerciseId); + Optional findWithExampleSubmissionsAndResultsAndGradingCriteriaById(@Param("exerciseId") long exerciseId); @EntityGraph(type = LOAD, attributePaths = { "studentParticipations", "studentParticipations.submissions", "studentParticipations.submissions.results" }) Optional findWithStudentParticipationsAndSubmissionsById(long exerciseId); @@ -62,6 +79,31 @@ public interface TextExerciseRepository extends ArtemisJpaRepository findWithGradingCriteriaById(long exerciseId); + @Query(""" + SELECT t + FROM TextExercise t + LEFT JOIN FETCH t.competencies + WHERE t.title = :title + AND t.course.id = :courseId + """) + Set findAllWithCompetenciesByTitleAndCourseId(@Param("title") String title, @Param("courseId") long courseId); + + /** + * Finds a text exercise by its title and course id and throws a NoUniqueQueryException if multiple exercises are found. + * + * @param title the title of the exercise + * @param courseId the id of the course + * @return the exercise with the given title and course id + * @throws NoUniqueQueryException if multiple exercises are found with the same title + */ + default Optional findUniqueWithCompetenciesByTitleAndCourseId(String title, long courseId) throws NoUniqueQueryException { + Set allExercises = findAllWithCompetenciesByTitleAndCourseId(title, courseId); + if (allExercises.size() > 1) { + throw new NoUniqueQueryException("Found multiple exercises with title " + title + " in course with id " + courseId); + } + return allExercises.stream().findFirst(); + } + @NotNull default TextExercise findWithGradingCriteriaByIdElseThrow(long exerciseId) { return getValueElseThrow(findWithGradingCriteriaById(exerciseId), exerciseId); @@ -77,6 +119,11 @@ default TextExercise findByIdWithExampleSubmissionsAndResultsElseThrow(long exer return getValueElseThrow(findWithExampleSubmissionsAndResultsById(exerciseId), exerciseId); } + @NotNull + default TextExercise findByIdWithExampleSubmissionsAndResultsAndGradingCriteriaElseThrow(long exerciseId) { + return getValueElseThrow(findWithExampleSubmissionsAndResultsAndGradingCriteriaById(exerciseId), exerciseId); + } + @NotNull default TextExercise findByIdWithStudentParticipationsAndSubmissionsElseThrow(long exerciseId) { return getValueElseThrow(findWithStudentParticipationsAndSubmissionsById(exerciseId), exerciseId); diff --git a/src/main/java/de/tum/cit/aet/artemis/text/web/admin/AdminTextAssessmentEventResource.java b/src/main/java/de/tum/cit/aet/artemis/text/web/admin/AdminTextAssessmentEventResource.java index 78b6a9f8871b..acecfb3b1e56 100644 --- a/src/main/java/de/tum/cit/aet/artemis/text/web/admin/AdminTextAssessmentEventResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/text/web/admin/AdminTextAssessmentEventResource.java @@ -19,6 +19,7 @@ * REST controller for administrating TextAssessmentEventResource. */ @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminTextAssessmentEventResource { @@ -36,7 +37,6 @@ public AdminTextAssessmentEventResource(TextAssessmentEventRepository textAssess * @return returns a List of TextAssessmentEvent's */ @GetMapping("event-insights/text-assessment/events/{courseId}") - @EnforceAdmin public ResponseEntity> getEventsByCourseId(@PathVariable Long courseId) { List events = textAssessmentEventRepository.findAllByCourseId(courseId); return ResponseEntity.ok().body(events); diff --git a/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/domain/TutorialGroup.java b/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/domain/TutorialGroup.java index 5a4f4c4b2f45..448f97e9ea1e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/domain/TutorialGroup.java +++ b/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/domain/TutorialGroup.java @@ -104,6 +104,18 @@ public class TutorialGroup extends DomainObject { @Transient private String teachingAssistantName; + /** + * This transient fields is set to the name of the teaching assistant of this tutorial group + */ + @Transient + private Long teachingAssistantId; + + /** + * This transient fields is set to the name of the teaching assistant of this tutorial group + */ + @Transient + private String teachingAssistantImageUrl; + /** * This transient fields is set to the course title to which this tutorial group belongs */ @@ -290,10 +302,30 @@ public String getTeachingAssistantName() { return teachingAssistantName; } + @JsonIgnore(false) + @JsonProperty + public Long getTeachingAssistantId() { + return teachingAssistantId; + } + + @JsonIgnore(false) + @JsonProperty + public String getTeachingAssistantImageUrl() { + return teachingAssistantImageUrl; + } + public void setTeachingAssistantName(String teachingAssistantName) { this.teachingAssistantName = teachingAssistantName; } + public void setTeachingAssistantId(Long teachingAssistantId) { + this.teachingAssistantId = teachingAssistantId; + } + + public void setTeachingAssistantImageUrl(String teachingAssistantImageUrl) { + this.teachingAssistantImageUrl = teachingAssistantImageUrl; + } + @JsonIgnore(false) @JsonProperty public String getCourseTitle() { diff --git a/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/service/TutorialGroupService.java b/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/service/TutorialGroupService.java index e339fc473e52..4f8ef7279c1e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/service/TutorialGroupService.java +++ b/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/service/TutorialGroupService.java @@ -114,10 +114,14 @@ public void setTransientPropertiesForUser(User user, TutorialGroup tutorialGroup if (getPersistenceUtil().isLoaded(tutorialGroup, "teachingAssistant") && tutorialGroup.getTeachingAssistant() != null) { tutorialGroup.setTeachingAssistantName(tutorialGroup.getTeachingAssistant().getName()); + tutorialGroup.setTeachingAssistantId(tutorialGroup.getTeachingAssistant().getId()); + tutorialGroup.setTeachingAssistantImageUrl(tutorialGroup.getTeachingAssistant().getImageUrl()); tutorialGroup.setIsUserTutor(tutorialGroup.getTeachingAssistant().equals(user)); } else { tutorialGroup.setTeachingAssistantName(null); + tutorialGroup.setTeachingAssistantId(null); + tutorialGroup.setTeachingAssistantImageUrl(null); } if (tutorialGroup.getTutorialGroupChannel() != null) { diff --git a/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupFreePeriodResource.java b/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupFreePeriodResource.java index d8a59414d71c..791eea50d82e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupFreePeriodResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupFreePeriodResource.java @@ -37,6 +37,7 @@ import de.tum.cit.aet.artemis.tutorialgroup.service.TutorialGroupFreePeriodService; @Profile(PROFILE_CORE) +@FeatureToggle(Feature.TutorialGroups) @RestController @RequestMapping("api/") public class TutorialGroupFreePeriodResource { @@ -73,7 +74,6 @@ public TutorialGroupFreePeriodResource(TutorialGroupsConfigurationRepository tut */ @GetMapping("courses/{courseId}/tutorial-groups-configuration/{tutorialGroupsConfigurationId}/tutorial-free-periods/{tutorialGroupFreePeriodId}") @EnforceAtLeastInstructor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity getOneOfConfiguration(@PathVariable Long courseId, @PathVariable Long tutorialGroupsConfigurationId, @PathVariable Long tutorialGroupFreePeriodId) { log.debug("REST request to get tutorial group free period: {} of tutorial group configuration {} of course: {}", tutorialGroupFreePeriodId, tutorialGroupsConfigurationId, @@ -96,7 +96,6 @@ public ResponseEntity getOneOfConfiguration(@PathVariab */ @PutMapping("courses/{courseId}/tutorial-groups-configuration/{tutorialGroupsConfigurationId}/tutorial-free-periods/{tutorialGroupFreePeriodId}") @EnforceAtLeastInstructor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity update(@PathVariable Long courseId, @PathVariable Long tutorialGroupsConfigurationId, @PathVariable Long tutorialGroupFreePeriodId, @RequestBody @Valid TutorialGroupFreePeriodDTO tutorialGroupFreePeriod) throws URISyntaxException { log.debug("REST request to update TutorialGroupFreePeriod: {} for tutorial group configuration: {} of course: {}", tutorialGroupFreePeriodId, tutorialGroupsConfigurationId, @@ -144,7 +143,6 @@ public ResponseEntity update(@PathVariable Long courseI */ @PostMapping("courses/{courseId}/tutorial-groups-configuration/{tutorialGroupsConfigurationId}/tutorial-free-periods") @EnforceAtLeastInstructor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity create(@PathVariable Long courseId, @PathVariable Long tutorialGroupsConfigurationId, @RequestBody @Valid TutorialGroupFreePeriodDTO tutorialGroupFreePeriod) throws URISyntaxException { log.debug("REST request to create TutorialGroupFreePeriod: {} for tutorial group configuration: {} of course: {}", tutorialGroupFreePeriod, tutorialGroupsConfigurationId, @@ -189,7 +187,6 @@ public ResponseEntity create(@PathVariable Long courseI */ @DeleteMapping("courses/{courseId}/tutorial-groups-configuration/{tutorialGroupsConfigurationId}/tutorial-free-periods/{tutorialGroupFreePeriodId}") @EnforceAtLeastInstructor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity delete(@PathVariable Long courseId, @PathVariable Long tutorialGroupsConfigurationId, @PathVariable Long tutorialGroupFreePeriodId) throws URISyntaxException { log.debug("REST request to delete TutorialGroupFreePeriod: {} of tutorial group configuration {} of course: {}", tutorialGroupFreePeriodId, tutorialGroupsConfigurationId, diff --git a/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupResource.java b/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupResource.java index daa50435cf94..a82e0c9297ad 100644 --- a/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupResource.java @@ -71,6 +71,7 @@ import de.tum.cit.aet.artemis.tutorialgroup.service.TutorialGroupService; @Profile(PROFILE_CORE) +@FeatureToggle(Feature.TutorialGroups) @RestController @RequestMapping("api/") public class TutorialGroupResource { @@ -131,7 +132,6 @@ public TutorialGroupResource(AuthorizationCheckService authorizationCheckService */ @GetMapping("tutorial-groups/{tutorialGroupId}/title") @EnforceAtLeastStudent - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity getTitle(@PathVariable Long tutorialGroupId) { log.debug("REST request to get title of TutorialGroup : {}", tutorialGroupId); return tutorialGroupRepository.getTutorialGroupTitle(tutorialGroupId).map(ResponseEntity::ok) @@ -147,7 +147,6 @@ public ResponseEntity getTitle(@PathVariable Long tutorialGroupId) { */ @GetMapping("courses/{courseId}/tutorial-groups/campus-values") @EnforceAtLeastInstructor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity> getUniqueCampusValues(@PathVariable Long courseId) { log.debug("REST request to get unique campus values used for tutorial groups in course : {}", courseId); var course = courseRepository.findByIdElseThrow(courseId); @@ -165,7 +164,6 @@ public ResponseEntity> getUniqueCampusValues(@PathVariable Long cour */ @GetMapping("courses/{courseId}/tutorial-groups/language-values") @EnforceAtLeastInstructor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity> getUniqueLanguageValues(@PathVariable Long courseId) { log.debug("REST request to get unique language values used for tutorial groups in course : {}", courseId); var course = courseRepository.findByIdElseThrow(courseId); @@ -182,7 +180,6 @@ public ResponseEntity> getUniqueLanguageValues(@PathVariable Long co */ @GetMapping("courses/{courseId}/tutorial-groups") @EnforceAtLeastStudent - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity> getAllForCourse(@PathVariable Long courseId) { log.debug("REST request to get all tutorial groups of course with id: {}", courseId); var course = courseRepository.findByIdElseThrow(courseId); @@ -202,7 +199,6 @@ public ResponseEntity> getAllForCourse(@PathVariable Long co */ @GetMapping("courses/{courseId}/tutorial-groups/{tutorialGroupId}") @EnforceAtLeastStudent - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity getOneOfCourse(@PathVariable Long courseId, @PathVariable Long tutorialGroupId) { log.debug("REST request to get tutorial group: {} of course: {}", tutorialGroupId, courseId); var course = courseRepository.findByIdElseThrow(courseId); @@ -221,7 +217,6 @@ public ResponseEntity getOneOfCourse(@PathVariable Long courseId, */ @PostMapping("courses/{courseId}/tutorial-groups") @EnforceAtLeastInstructor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity create(@PathVariable Long courseId, @RequestBody @Valid TutorialGroup tutorialGroup) throws URISyntaxException { log.debug("REST request to create TutorialGroup: {} in course: {}", tutorialGroup, courseId); if (tutorialGroup.getId() != null) { @@ -282,7 +277,6 @@ public ResponseEntity create(@PathVariable Long courseId, @Reques */ @DeleteMapping("courses/{courseId}/tutorial-groups/{tutorialGroupId}") @EnforceAtLeastInstructor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity delete(@PathVariable Long courseId, @PathVariable Long tutorialGroupId) { log.info("REST request to delete a TutorialGroup: {} of course: {}", tutorialGroupId, courseId); var tutorialGroupFromDatabase = this.tutorialGroupRepository.findByIdWithTeachingAssistantAndRegistrationsElseThrow(tutorialGroupId); @@ -317,7 +311,6 @@ public record TutorialGroupUpdateDTO(@Valid @NotNull TutorialGroup tutorialGroup */ @PutMapping("courses/{courseId}/tutorial-groups/{tutorialGroupId}") @EnforceAtLeastInstructor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity update(@PathVariable long courseId, @PathVariable long tutorialGroupId, @RequestBody @Valid TutorialGroupUpdateDTO tutorialGroupUpdateDTO) { TutorialGroup updatedTutorialGroup = tutorialGroupUpdateDTO.tutorialGroup(); @@ -405,7 +398,6 @@ public ResponseEntity update(@PathVariable long courseId, @PathVa */ @DeleteMapping("courses/{courseId}/tutorial-groups/{tutorialGroupId}/deregister/{studentLogin:" + Constants.LOGIN_REGEX + "}") @EnforceAtLeastTutor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity deregisterStudent(@PathVariable Long courseId, @PathVariable Long tutorialGroupId, @PathVariable String studentLogin) { log.debug("REST request to deregister {} student from tutorial group : {}", studentLogin, tutorialGroupId); var tutorialGroupFromDatabase = this.tutorialGroupRepository.findByIdElseThrow(tutorialGroupId); @@ -427,7 +419,6 @@ public ResponseEntity deregisterStudent(@PathVariable Long courseId, @Path */ @PostMapping("courses/{courseId}/tutorial-groups/{tutorialGroupId}/register/{studentLogin:" + Constants.LOGIN_REGEX + "}") @EnforceAtLeastTutor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity registerStudent(@PathVariable Long courseId, @PathVariable Long tutorialGroupId, @PathVariable String studentLogin) { log.debug("REST request to register {} student to tutorial group : {}", studentLogin, tutorialGroupId); var tutorialGroupFromDatabase = this.tutorialGroupRepository.findByIdElseThrow(tutorialGroupId); @@ -454,7 +445,6 @@ public ResponseEntity registerStudent(@PathVariable Long courseId, @PathVa */ @PostMapping("courses/{courseId}/tutorial-groups/{tutorialGroupId}/register-multiple") @EnforceAtLeastInstructor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity> registerMultipleStudentsToTutorialGroup(@PathVariable long courseId, @PathVariable long tutorialGroupId, @RequestBody Set studentDtos) { log.debug("REST request to register {} to tutorial group {}", studentDtos, tutorialGroupId); @@ -476,7 +466,6 @@ public ResponseEntity> registerMultipleStudentsToTutorialGroup(@ */ @PostMapping("courses/{courseId}/tutorial-groups/import") @EnforceAtLeastInstructor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity> importRegistrations(@PathVariable Long courseId, @RequestBody @Valid Set importDTOs) { log.debug("REST request to import registrations {} to course {}", importDTOs, courseId); @@ -549,7 +538,6 @@ private void checkEntityIdMatchesPathIds(TutorialGroup tutorialGroup, Optional exportTutorialGroupsToCSV(@PathVariable Long courseId, @RequestParam List fields) { log.debug("REST request to export TutorialGroups to CSV for course: {}", courseId); var course = courseRepository.findByIdElseThrow(courseId); @@ -580,7 +568,6 @@ public ResponseEntity exportTutorialGroupsToCSV(@PathVariable Long cours */ @GetMapping(value = "courses/{courseId}/tutorial-groups/export/json", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastInstructorInCourse - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity exportTutorialGroupsToJSON(@PathVariable Long courseId, @RequestParam List fields) { log.debug("REST request to export TutorialGroups to JSON for course: {}", courseId); try { diff --git a/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupSessionResource.java b/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupSessionResource.java index 54790e628ba9..4d9c9459f65c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupSessionResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupSessionResource.java @@ -56,6 +56,7 @@ import de.tum.cit.aet.artemis.tutorialgroup.service.TutorialGroupService; @Profile(PROFILE_CORE) +@FeatureToggle(Feature.TutorialGroups) @RestController @RequestMapping("api/") public class TutorialGroupSessionResource { @@ -101,7 +102,6 @@ public TutorialGroupSessionResource(TutorialGroupSessionRepository tutorialGroup */ @GetMapping("courses/{courseId}/tutorial-groups/{tutorialGroupId}/sessions/{sessionId}") @EnforceAtLeastStudent - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity getOneOfTutorialGroup(@PathVariable Long courseId, @PathVariable Long tutorialGroupId, @PathVariable Long sessionId) { log.debug("REST request to get session: {} of tutorial group: {} of course {}", sessionId, tutorialGroupId, courseId); var session = tutorialGroupSessionRepository.findByIdElseThrow(sessionId); @@ -124,7 +124,6 @@ public ResponseEntity getOneOfTutorialGroup(@PathVariable */ @PutMapping("courses/{courseId}/tutorial-groups/{tutorialGroupId}/sessions/{sessionId}") @EnforceAtLeastTutor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity update(@PathVariable Long courseId, @PathVariable Long tutorialGroupId, @PathVariable Long sessionId, @RequestBody @Valid TutorialGroupSessionDTO tutorialGroupSessionDTO) { log.debug("REST request to update session: {} of tutorial group: {} of course {}", sessionId, tutorialGroupId, courseId); @@ -170,7 +169,6 @@ public ResponseEntity update(@PathVariable Long courseId, */ @PatchMapping("courses/{courseId}/tutorial-groups/{tutorialGroupId}/sessions/{sessionId}/attendance-count") @EnforceAtLeastTutor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity updateAttendanceCount(@PathVariable Long courseId, @PathVariable Long tutorialGroupId, @PathVariable Long sessionId, @RequestParam(required = false) @Min(0) @Max(3000) Integer attendanceCount) { log.debug("REST request to update attendance count of session: {} of tutorial group: {} of course {} to {}", sessionId, tutorialGroupId, courseId, attendanceCount); @@ -192,7 +190,6 @@ public ResponseEntity updateAttendanceCount(@PathVariable */ @DeleteMapping("courses/{courseId}/tutorial-groups/{tutorialGroupId}/sessions/{sessionId}") @EnforceAtLeastTutor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity deleteSession(@PathVariable Long courseId, @PathVariable Long tutorialGroupId, @PathVariable Long sessionId) { log.debug("REST request to delete session: {} of tutorial group: {} of course {}", sessionId, tutorialGroupId, courseId); var sessionFromDatabase = this.tutorialGroupSessionRepository.findByIdElseThrow(sessionId); @@ -212,7 +209,6 @@ public ResponseEntity deleteSession(@PathVariable Long courseId, @PathVari */ @PostMapping("courses/{courseId}/tutorial-groups/{tutorialGroupId}/sessions") @EnforceAtLeastTutor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity create(@PathVariable Long courseId, @PathVariable Long tutorialGroupId, @RequestBody @Valid TutorialGroupSessionDTO tutorialGroupSessionDTO) throws URISyntaxException { log.debug("REST request to create TutorialGroupSession: {} for tutorial group: {}", tutorialGroupSessionDTO, tutorialGroupId); @@ -255,7 +251,6 @@ private TutorialGroupsConfiguration validateTutorialGroupConfiguration(@PathVari */ @PostMapping("courses/{courseId}/tutorial-groups/{tutorialGroupId}/sessions/{sessionId}/cancel") @EnforceAtLeastTutor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity cancel(@PathVariable Long courseId, @PathVariable Long tutorialGroupId, @PathVariable Long sessionId, @RequestBody TutorialGroupStatusDTO tutorialGroupStatusDTO) throws URISyntaxException { log.debug("REST request to cancel session: {} of tutorial group: {} of course {}", sessionId, tutorialGroupId, courseId); @@ -283,7 +278,6 @@ public ResponseEntity cancel(@PathVariable Long courseId, */ @PostMapping("courses/{courseId}/tutorial-groups/{tutorialGroupId}/sessions/{sessionId}/activate") @EnforceAtLeastTutor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity activate(@PathVariable long courseId, @PathVariable long tutorialGroupId, @PathVariable long sessionId) throws URISyntaxException { log.debug("REST request to activate session: {} of tutorial group: {} of course {}", sessionId, tutorialGroupId, courseId); var sessionToActivate = tutorialGroupSessionRepository.findByIdElseThrow(sessionId); diff --git a/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupsConfigurationResource.java b/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupsConfigurationResource.java index 4e9c9212ee4b..dc26c5bd4e09 100644 --- a/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupsConfigurationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupsConfigurationResource.java @@ -36,6 +36,7 @@ import de.tum.cit.aet.artemis.tutorialgroup.service.TutorialGroupChannelManagementService; @Profile(PROFILE_CORE) +@FeatureToggle(Feature.TutorialGroups) @RestController @RequestMapping("api/") public class TutorialGroupsConfigurationResource { @@ -68,7 +69,6 @@ public TutorialGroupsConfigurationResource(TutorialGroupsConfigurationRepository */ @GetMapping("courses/{courseId}/tutorial-groups-configuration") @EnforceAtLeastStudent - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity getOneOfCourse(@PathVariable Long courseId) { log.debug("REST request to get tutorial groups configuration of course: {}", courseId); var course = courseRepository.findByIdElseThrow(courseId); @@ -85,7 +85,6 @@ public ResponseEntity getOneOfCourse(@PathVariable */ @PostMapping("courses/{courseId}/tutorial-groups-configuration") @EnforceAtLeastInstructor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity create(@PathVariable Long courseId, @RequestBody @Valid TutorialGroupsConfiguration tutorialGroupsConfiguration) throws URISyntaxException { log.debug("REST request to create TutorialGroupsConfiguration: {} for course: {}", tutorialGroupsConfiguration, courseId); @@ -120,7 +119,6 @@ public ResponseEntity create(@PathVariable Long cou */ @PutMapping("courses/{courseId}/tutorial-groups-configuration/{tutorialGroupsConfigurationId}") @EnforceAtLeastInstructor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity update(@PathVariable Long courseId, @PathVariable Long tutorialGroupsConfigurationId, @RequestBody @Valid TutorialGroupsConfiguration updatedTutorialGroupConfiguration) { log.debug("REST request to update TutorialGroupsConfiguration: {} of course: {}", updatedTutorialGroupConfiguration, courseId); diff --git a/src/main/resources/config/application-buildagent.yml b/src/main/resources/config/application-buildagent.yml index b013910100e5..1439567b2cc9 100644 --- a/src/main/resources/config/application-buildagent.yml +++ b/src/main/resources/config/application-buildagent.yml @@ -18,7 +18,7 @@ artemis: specify-concurrent-builds: false concurrent-build-size: 1 asynchronous: true - timeout-seconds: 240 + timeout-seconds: 120 build-container-prefix: local-ci- proxies: use-system-proxy: false diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml index 924d087ec8f2..3924e2d804f9 100644 --- a/src/main/resources/config/application.yml +++ b/src/main/resources/config/application.yml @@ -91,6 +91,8 @@ artemis: default: "ghcr.io/ls1intum/artemis-rust-docker:v0.9.70" javascript: default: "ghcr.io/ls1intum/artemis-javascript-docker:v1.0.0" + r: + default: "ghcr.io/ls1intum/artemis-r-docker:v1.0.0" management: endpoints: diff --git a/src/main/resources/config/liquibase/changelog/20240816150000_changelog.xml b/src/main/resources/config/liquibase/changelog/20240816150000_changelog.xml new file mode 100644 index 000000000000..b3fa6c9e1c47 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20240816150000_changelog.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20240825191919_changelog.xml b/src/main/resources/config/liquibase/changelog/20240825191919_changelog.xml new file mode 100644 index 000000000000..3ae7fd7ea038 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20240825191919_changelog.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + DELETE FROM iris_sub_settings WHERE discriminator = 'HESTIA'; + + + + + + + + + + + UPDATE iris_sub_settings + SET allowed_variants = 'default', selected_variant = 'default' + WHERE id IN ( + SELECT iris_chat_settings_id FROM iris_settings WHERE discriminator = 'GLOBAL' + UNION + SELECT iris_competency_generation_settings_id FROM iris_settings WHERE discriminator = 'GLOBAL' + UNION + SELECT iris_lecture_ingestion_settings_id FROM iris_settings WHERE discriminator = 'GLOBAL' + ); + + + + + + + + + + + + + + DELETE FROM iris_json_message_content WHERE id IN ( + SELECT iris_message_content.id FROM iris_message_content + JOIN iris_message ON iris_message_content.message_id = iris_message.id + JOIN iris_session ON iris_message.session_id = iris_session.id + WHERE iris_session.discriminator = 'HESTIA' + ); + DELETE FROM iris_text_message_content WHERE id IN ( + SELECT iris_message_content.id FROM iris_message_content + JOIN iris_message ON iris_message_content.message_id = iris_message.id + JOIN iris_session ON iris_message.session_id = iris_session.id + WHERE iris_session.discriminator = 'HESTIA' + ); + DELETE FROM iris_message_content WHERE message_id IN ( + SELECT iris_message.id FROM iris_message + JOIN iris_session ON iris_message.session_id = iris_session.id + WHERE iris_session.discriminator = 'HESTIA' + ); + DELETE FROM iris_message WHERE session_id IN ( + SELECT id FROM iris_session WHERE discriminator = 'HESTIA' + ); + DELETE FROM iris_session WHERE discriminator = 'HESTIA'; + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20240902175045_changelog.xml b/src/main/resources/config/liquibase/changelog/20240902175045_changelog.xml new file mode 100644 index 000000000000..6344b448df92 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20240902175045_changelog.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20240909090909_changelog.xml b/src/main/resources/config/liquibase/changelog/20240909090909_changelog.xml new file mode 100644 index 000000000000..86b235b1d655 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20240909090909_changelog.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20240924125742_changelog.xml b/src/main/resources/config/liquibase/changelog/20240924125742_changelog.xml new file mode 100644 index 000000000000..c74139c77c6d --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20240924125742_changelog.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index 2c204094c0ff..d496528a13ec 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -22,7 +22,12 @@ + + + + + diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index 1b148cca8b2b..0879e526d4f4 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -79,8 +79,6 @@ email.notification.aux.information.release.date=Release Date : {0} email.notification.aux.information.due.date=Due Date : {0} email.notification.aux.information.submission.date=Submission Date : {0} -email.notification.aux.notification.post.content=Content: - # Exercise Types email.notification.aux.exercise.type.quiz=The quiz exercise email.notification.aux.exercise.type.programming=The programming exercise diff --git a/src/main/resources/i18n/messages_de.properties b/src/main/resources/i18n/messages_de.properties index f1fa989cf5f7..289a66b117a2 100644 --- a/src/main/resources/i18n/messages_de.properties +++ b/src/main/resources/i18n/messages_de.properties @@ -15,7 +15,7 @@ email.activation.text2=Grüße email.signature=Das Artemis Team. # Creation email -email.creation.text1=Dein Artemis Zugang wurde angelegt, bitte klicke auf den Link um dich anzumelden: +email.creation.text1=Dein Artemis Zugang wurde angelegt, bitte klicke auf den Link, um dich anzumelden: # Reset email email.reset.title=Artemis Passwort zurücksetzen @@ -26,7 +26,7 @@ email.reset.text2=Grüße, # SAML2 Account created email.saml.title=Artemis Account angelegt email.saml.greeting=Liebe(r) {0} -email.saml.text1=Dein Artemis Account wurde angelegt. Setze über den Link ein lokales App-Passwort, um auf Artemis und die verknüpften Dienste (Git, Build-Server,...) zuzugreifen. +email.saml.text1=Dein Artemis Account wurde angelegt. Setze über den Link ein lokales App-Passwort, um auf Artemis und die verknüpften Dienste (Git, Build-Server, ...) zuzugreifen. email.saml.text2=Nach Ablauf des Links kann das Passwort weiterhin über die "Passwort vergessen"-Funktion gesetzt werden. email.saml.text3=Grüße, email.saml.username=Nutzername: {0} @@ -48,7 +48,7 @@ email.notification.group.editors="Editor:innen" # Notification Titles (based on originating type) email.notification.title.attachment=Der Anhang "{0}" für die Vorlesung "{1}" in dem Kurs "{2}" wurde aktualisiert. -email.notification.title.file.submission.successful=Die Einreichung der Dateiupload-aufgabe "{0}" in dem Kurs "{1}" war erfolgreich. +email.notification.title.file.submission.successful=Die Einreichung der Dateiupload-Aufgabe "{0}" in dem Kurs "{1}" war erfolgreich. email.notification.title.exercise.submission.assessed=Die eingereichte Lösung für die Aufgabe "{0}" in dem Kurs "{1}" wurde korrigiert. email.notification.title.duplicate.test.cases="{0}" in dem Kurs"{1}" hat mehrere Testfälle mit gleichen Namen! Dieser kritische Fehler sollte so früh wie möglich korrigiert werden, sonst treten Probleme bei der Erstellung von Ergebnissen für Studierende auf! @@ -61,7 +61,7 @@ email.notification.title.exercise.practice="{0}" im Kurs "{1}" wurde zum Üben f # Exercise Info email.notification.title.exercise.information=Informationen zu der Aufgabe: -email.notification.title.exercise.information.difficulty=Schwierigkeitsstufe : {0} +email.notification.title.exercise.information.difficulty=Schwierigkeitsstufe: {0} email.notification.title.exercise.information.max=Anzahl an Punkten: {0} email.notification.title.exercise.information.bonus=Anzahl an Bonus Punkten: {0} email.notification.title.exercise.information.possible=Anzahl maximal erreichbarer Punkte: {0} @@ -69,8 +69,8 @@ email.notification.aux.information.exercise.score=Dein erreichtes Ergebnis: {0}% # Auxiliary -email.notification.aux.notification.text.header.change.message=Änderungsnachricht : -email.notification.aux.footer=Diese und ähnliche Emails können (de)aktiviert werden: +email.notification.aux.notification.text.header.change.message=Änderungsnachricht: +email.notification.aux.footer=Diese und ähnliche E-Mails können (de)aktiviert werden: email.notification.aux.footer.link=Benachrichtigungseinstellungen in Artemis email.notification.aux.open.button=In Artemis öffnen email.notification.aux.emergency.link.text=Alternativ kann auch folgender Link verwendet werden: @@ -79,8 +79,6 @@ email.notification.aux.information.release.date=Veröffentlichungsdatum: {0} email.notification.aux.information.due.date=Abgabezeitpunkt: {0} email.notification.aux.information.submission.date=Einreichungsdatum: {0} -email.notification.aux.notification.post.content=Inhalt: - # Exercise Types email.notification.aux.exercise.type.quiz=Die Quizaufgabe email.notification.aux.exercise.type.programming=Die Programmieraufgabe @@ -95,7 +93,7 @@ email.notification.aux.difficulty.hard=Schwer # Plagiarism email.plagiarism.title=Neuer Plagiatsfall: Übung "{0}" im Kurs "{1}" -email.plagiarism.cpc.title=Neue signifikante bereinstimmung: Aufgabe "{0}" im Kurs "{1}" +email.plagiarism.cpc.title=Neue signifikante ?bereinstimmung: Aufgabe "{0}" im Kurs "{1}" email.notification.title.post.plagiarismVerdict=Entscheidung zum Plagiatsfall in der Aufgabe {0} gefallen email.notification.aux.plagiarismVerdict.plagiarism=Der Fall wird als Plagiat angesehen! email.notification.aux.plagiarismVerdict.point.deduction=Wegen des Plagiatsfalls ziehen wir dir Punkte in der Aufgabe ab! @@ -124,8 +122,8 @@ email.dataExportFailedAdmin.actionItemList = Bitte führe die folgenden beiden A email.dataExportFailedAdmin.actionItem1 = \u2022 Stelle sicher, dass die Konfiguration deiner Artemis Instanz korrekt ist. email.dataExportFailedAdmin.actionItem2 = \u2022 Falls du weitere Hilfe benötigst, kontaktiere das Artemis Entwicklungsteam, indem du mit dem folgenden Link ein Issue auf GitHub erstellst: email.dataExportFailedAdmin.githubLink = Link um ein Issue im Artemis GitHub Projekt anzulegen -email.successfulDataExportCreationsAdmin.title = Angeforderte Datenexporte wurden fr deine Instanz erfolgreich erstellt -email.successfulDataExportCreationsAdmin.text = Datenexporte fr die folgenden Nutzer wurden erfolgreich erstellt als der Job um die Datenexporte zu erstellen zuletzt ausgefhrt wurde: +email.successfulDataExportCreationsAdmin.title = Angeforderte Datenexporte wurden f?r deine Instanz erfolgreich erstellt +email.successfulDataExportCreationsAdmin.text = Datenexporte f?r die folgenden Nutzer wurden erfolgreich erstellt als der Job um die Datenexporte zu erstellen zuletzt ausgef?hrt wurde: email.successfulDataExportCreationsAdmin.userLogin = \u2022 {0} # Email Subjects # The reason for the format artemisApp.{notificationCategory}.title.{notificicationType} is that these placeholders are also used in the client and this is the format used there @@ -138,7 +136,7 @@ artemisApp.groupNotification.title.newAnnouncementPost = Neue Ankündigung artemisApp.singleUserNotification.title.exerciseSubmissionAssessed = Übungsabgabe bewertet artemisApp.singleUserNotification.title.fileSubmissionSuccessful = Dateiabgabe erfolgreich artemisApp.singleUserNotification.title.newPlagiarismCaseStudent = Neuer Plagiatsfall -artemisApp.singleUserNotification.title.newPlagiarismCaseStudentSignificantSimilarity = Neue signifikante bereinstimmung +artemisApp.singleUserNotification.title.newPlagiarismCaseStudentSignificantSimilarity = Neue signifikante ?bereinstimmung artemisApp.singleUserNotification.title.plagiarismCaseVerdictStudent = Urteil zu deinem Plagiatsfall artemisApp.singleUserNotification.title.tutorialGroupRegistrationStudent = Du wurdest für eine Übungsgruppe registriert artemisApp.singleUserNotification.title.tutorialGroupDeregistrationStudent = Du wurdest von einer Übungsgruppe abgemeldet diff --git a/src/main/resources/i18n/messages_en.properties b/src/main/resources/i18n/messages_en.properties index 8895514a5364..ef2bc15f8c9b 100644 --- a/src/main/resources/i18n/messages_en.properties +++ b/src/main/resources/i18n/messages_en.properties @@ -29,7 +29,7 @@ email.saml.greeting=Dear {0} email.saml.text1=Your Artemis account has been created. A local Artemis password is only needed to access Git and build services. To create your local Artemis password click the link below: email.saml.text2=After expiration of this link you can use the "password-reset" button. email.saml.text3=Regards, -email.saml.username=User name: {0} +email.saml.username=Username: {0} email.saml.email=E-Mail: {0} # Weekly summary email @@ -79,8 +79,6 @@ email.notification.aux.information.release.date=Release Date : {0} email.notification.aux.information.due.date=Due Date : {0} email.notification.aux.information.submission.date=Submission Date : {0} -email.notification.aux.notification.post.content=Content: - # Exercise Types email.notification.aux.exercise.type.quiz=The quiz exercise email.notification.aux.exercise.type.programming=The programming exercise @@ -121,11 +119,11 @@ email.dataExportFailedAdmin.text = The data export for the user with the login email.dataExportFailedAdmin.textFailed = failed. email.dataExportFailedAdmin.reason = The exception message was the following: {0} email.dataExportFailedAdmin.actionItemList = Please complete the following action items: -email.dataExportFailedAdmin.actionItem1 = \u2022 Make sure the configuration for your Artemis instance is correct. +email.dataExportFailedAdmin.actionItem1 = \u2022 Make sure the configuration for your Artemis instance is correct. email.dataExportFailedAdmin.actionItem2 = \u2022 If you need further help, please contact the Artemis developers by opening an issue on GitHub using the link below: email.dataExportFailedAdmin.githubLink = Link to open an issue on the Artemis GitHub project email.successfulDataExportCreationsAdmin.title = Successfully created requested data exports for your instance -email.successfulDataExportCreationsAdmin.text = Data exports for the following users were successfully created when the data export creation job was ran: +email.successfulDataExportCreationsAdmin.text = Data exports for the following users were successfully created when the data export creation job was running: email.successfulDataExportCreationsAdmin.userLogin = \u2022 {0} # Email Subjects diff --git a/src/main/resources/templates/aeolus/assembler/default.sh b/src/main/resources/templates/aeolus/assembler/default.sh index cca62d776297..e5a322facd87 100644 --- a/src/main/resources/templates/aeolus/assembler/default.sh +++ b/src/main/resources/templates/aeolus/assembler/default.sh @@ -8,15 +8,15 @@ provide_environment_information () { python3 --version pip3 --version echo "--------------------Contents of tests repository--------------------" - ls -la tests + ls -la ${testWorkingDirectory} echo "---------------------------------------------" echo "--------------------Contents of assignment repository--------------------" - ls -la assignment + ls -la ${studentParentWorkingDirectoryName} echo "---------------------------------------------" #Fallback in case Docker does not work as intended - REQ_FILE=tests/requirements.txt + REQ_FILE=${testWorkingDirectory}/requirements.txt if [ -f "$REQ_FILE" ]; then - pip3 install --user -r tests/requirements.txt + pip3 install --user -r ${testWorkingDirectory}/requirements.txt else echo "$REQ_FILE does not exist" fi @@ -25,18 +25,18 @@ provide_environment_information () { prepare_makefile () { echo '⚙️ executing prepare_makefile' #!/usr/bin/env bash - rm -f assignment/{GNUmakefile, Makefile, makefile} - rm -f assignment/io.inc - cp -f tests/Makefile assignment/Makefile || exit 2 - cp -f tests/io.inc assignment/io.inc || exit 2 + rm -f ${studentParentWorkingDirectoryName}/{GNUmakefile, Makefile, makefile} + rm -f ${studentParentWorkingDirectoryName}/io.inc + cp -f ${testWorkingDirectory}/Makefile ${studentParentWorkingDirectoryName}/Makefile || exit 2 + cp -f ${testWorkingDirectory}/io.inc ${studentParentWorkingDirectoryName}/io.inc || exit 2 } run_and_compile () { echo '⚙️ executing run_and_compile' - cd tests - python3 compileTest.py ../assignment/ + cd ${testWorkingDirectory} + python3 compileTest.py ../${studentParentWorkingDirectoryName}/ rm compileTest.py - cp result.xml ../assignment/result.xml + cp result.xml ../${studentParentWorkingDirectoryName}/result.xml } junit () { diff --git a/src/main/resources/templates/aeolus/assembler/default.yaml b/src/main/resources/templates/aeolus/assembler/default.yaml index 3017be0a331b..7649ec646a51 100644 --- a/src/main/resources/templates/aeolus/assembler/default.yaml +++ b/src/main/resources/templates/aeolus/assembler/default.yaml @@ -7,15 +7,15 @@ actions: python3 --version pip3 --version echo "--------------------Contents of tests repository--------------------" - ls -la tests + ls -la ${testWorkingDirectory} echo "---------------------------------------------" echo "--------------------Contents of assignment repository--------------------" - ls -la assignment + ls -la ${studentParentWorkingDirectoryName} echo "---------------------------------------------" #Fallback in case Docker does not work as intended - REQ_FILE=tests/requirements.txt + REQ_FILE=${testWorkingDirectory}/requirements.txt if [ -f "$REQ_FILE" ]; then - pip3 install --user -r tests/requirements.txt + pip3 install --user -r ${testWorkingDirectory}/requirements.txt else echo "$REQ_FILE does not exist" fi @@ -23,17 +23,17 @@ actions: - name: prepare_makefile script: |- #!/usr/bin/env bash - rm -f assignment/{GNUmakefile, Makefile, makefile} - rm -f assignment/io.inc - cp -f tests/Makefile assignment/Makefile || exit 2 - cp -f tests/io.inc assignment/io.inc || exit 2 + rm -f ${studentParentWorkingDirectoryName}/{GNUmakefile, Makefile, makefile} + rm -f ${studentParentWorkingDirectoryName}/io.inc + cp -f ${testWorkingDirectory}/Makefile ${studentParentWorkingDirectoryName}/Makefile || exit 2 + cp -f ${testWorkingDirectory}/io.inc ${studentParentWorkingDirectoryName}/io.inc || exit 2 runAlways: false - name: run_and_compile script: |- - cd tests - python3 compileTest.py ../assignment/ + cd ${testWorkingDirectory} + python3 compileTest.py ../${studentParentWorkingDirectoryName}/ rm compileTest.py - cp result.xml ../assignment/result.xml + cp result.xml ../${studentParentWorkingDirectoryName}/result.xml runAlways: false - name: junit script: |- @@ -41,6 +41,6 @@ actions: runAlways: true results: - name: junit_result.xml - path: assignment/result.xml + path: ${studentParentWorkingDirectoryName}/result.xml type: junit before: true diff --git a/src/main/resources/templates/aeolus/c/fact.sh b/src/main/resources/templates/aeolus/c/fact.sh index 3596904b322b..4f7252eefaba 100644 --- a/src/main/resources/templates/aeolus/c/fact.sh +++ b/src/main/resources/templates/aeolus/c/fact.sh @@ -8,8 +8,8 @@ setup_the_build_environment () { # Task Description: # Build and run all tests # ------------------------------ - # Updating assignment and test-reports ownership... - sudo chown artemis_user:artemis_user assignment/ -R || true + # Updating ${studentParentWorkingDirectoryName} and test-reports ownership... + sudo chown artemis_user:artemis_user ${studentParentWorkingDirectoryName}/ -R || true sudo mkdir test-reports sudo chown artemis_user:artemis_user test-reports/ -R || true } @@ -22,13 +22,13 @@ build_and_run_all_tests () { # Build and run all tests # ------------------------------ - rm -f assignment/GNUmakefile - rm -f assignment/Makefile - cp -f tests/Makefile assignment/Makefile || exit 2 - cd tests + rm -f ${studentParentWorkingDirectoryName}/GNUmakefile + rm -f ${studentParentWorkingDirectoryName}/Makefile + cp -f ${testWorkingDirectory}/Makefile ${studentParentWorkingDirectoryName}/Makefile || exit 2 + cd ${testWorkingDirectory} python3 Tests.py rm Tests.py - rm -rf ./tests || true + rm -rf ./${testWorkingDirectory} || true } main () { diff --git a/src/main/resources/templates/aeolus/c/fact.yaml b/src/main/resources/templates/aeolus/c/fact.yaml index 2cca1f9526b3..9d3527e65503 100644 --- a/src/main/resources/templates/aeolus/c/fact.yaml +++ b/src/main/resources/templates/aeolus/c/fact.yaml @@ -8,7 +8,7 @@ actions: # Build and run all tests # ------------------------------ # Updating assignment and test-reports ownership... - sudo chown artemis_user:artemis_user assignment/ -R || true + sudo chown artemis_user:artemis_user ${studentParentWorkingDirectoryName}/ -R || true sudo mkdir test-reports sudo chown artemis_user:artemis_user test-reports/ -R || true runAlways: false @@ -20,13 +20,13 @@ actions: # Build and run all tests # ------------------------------ - rm -f assignment/GNUmakefile - rm -f assignment/Makefile - cp -f tests/Makefile assignment/Makefile || exit 2 - cd tests + rm -f ${studentParentWorkingDirectoryName}/GNUmakefile + rm -f ${studentParentWorkingDirectoryName}/Makefile + cp -f ${testWorkingDirectory}/Makefile ${studentParentWorkingDirectoryName}/Makefile || exit 2 + cd ${testWorkingDirectory} python3 Tests.py rm Tests.py - rm -rf ./tests || true + rm -rf ./${testWorkingDirectory} || true runAlways: false results: - name: junit_test-reports/tests-results.xml diff --git a/src/main/resources/templates/aeolus/c/gcc.sh b/src/main/resources/templates/aeolus/c/gcc.sh index 259f0bd886e0..3272660657a1 100644 --- a/src/main/resources/templates/aeolus/c/gcc.sh +++ b/src/main/resources/templates/aeolus/c/gcc.sh @@ -10,13 +10,13 @@ setup_the_build_environment () { # Build and run all tests # ------------------------------ - # Updating assignment and test-reports ownership... - sudo chown artemis_user:artemis_user assignment/ -R + # Updating ${studentParentWorkingDirectoryName} and test-reports ownership... + sudo chown artemis_user:artemis_user ${studentParentWorkingDirectoryName}/ -R mkdir test-reports chown artemis_user:artemis_user test-reports/ -R # assignment - cd tests + cd ${testWorkingDirectory} REQ_FILE=requirements.txt if [ -f "$REQ_FILE" ]; then pip3 install --user -r requirements.txt || true @@ -35,18 +35,18 @@ setup_makefile () { # Setup makefile # ------------------------------ - shadowFilePath="../tests/testUtils/c/shadow_exec.c" + shadowFilePath="../${testWorkingDirectory}/testUtils/c/shadow_exec.c" - foundIncludeDirs=`grep -m 1 'INCLUDEDIRS\s*=' assignment/Makefile` + foundIncludeDirs=`grep -m 1 'INCLUDEDIRS\s*=' ${studentParentWorkingDirectoryName}/Makefile` - foundSource=`grep -m 1 'SOURCE\s*=' assignment/Makefile` + foundSource=`grep -m 1 'SOURCE\s*=' ${studentParentWorkingDirectoryName}/Makefile` foundSource="$foundSource $shadowFilePath" - rm -f assignment/GNUmakefile - rm -f assignment/makefile + rm -f ${studentParentWorkingDirectoryName}/GNUmakefile + rm -f ${studentParentWorkingDirectoryName}/makefile - cp -f tests/Makefile assignment/Makefile || exit 2 - sed -i "s~\bINCLUDEDIRS\s*=.*~${foundIncludeDirs}~; s~\bSOURCE\s*=.*~${foundSource}~" assignment/Makefile + cp -f ${testWorkingDirectory}/Makefile ${studentParentWorkingDirectoryName}/Makefile || exit 2 + sed -i "s~\bINCLUDEDIRS\s*=.*~${foundIncludeDirs}~; s~\bSOURCE\s*=.*~${foundSource}~" ${studentParentWorkingDirectoryName}/Makefile } build_and_run_all_tests () { @@ -58,10 +58,10 @@ build_and_run_all_tests () { # Build and run all tests if the compilation succeeds # ------------------------------ sudo chown artemis_user:artemis_user . - gcc -c -Wall assignment/*.c || error=true + gcc -c -Wall ${studentParentWorkingDirectoryName}/*.c || error=true if [ ! $error ] then - cd tests || exit 0 + cd ${testWorkingDirectory} || exit 0 python3 Tests.py || true fi } diff --git a/src/main/resources/templates/aeolus/c/gcc.yaml b/src/main/resources/templates/aeolus/c/gcc.yaml index 622b2148279d..29b2e2c635ec 100644 --- a/src/main/resources/templates/aeolus/c/gcc.yaml +++ b/src/main/resources/templates/aeolus/c/gcc.yaml @@ -10,12 +10,12 @@ actions: # ------------------------------ # Updating assignment and test-reports ownership... - sudo chown artemis_user:artemis_user assignment/ -R + sudo chown artemis_user:artemis_user ${studentParentWorkingDirectoryName}/ -R mkdir test-reports chown artemis_user:artemis_user test-reports/ -R # assignment - cd tests + cd ${testWorkingDirectory} REQ_FILE=requirements.txt if [ -f "$REQ_FILE" ]; then pip3 install --user -r requirements.txt || true @@ -33,18 +33,18 @@ actions: # Setup makefile # ------------------------------ - shadowFilePath="../tests/testUtils/c/shadow_exec.c" + shadowFilePath="../${testWorkingDirectory}/testUtils/c/shadow_exec.c" - foundIncludeDirs=`grep -m 1 'INCLUDEDIRS\s*=' assignment/Makefile` + foundIncludeDirs=`grep -m 1 'INCLUDEDIRS\s*=' ${studentParentWorkingDirectoryName}/Makefile` - foundSource=`grep -m 1 'SOURCE\s*=' assignment/Makefile` + foundSource=`grep -m 1 'SOURCE\s*=' ${studentParentWorkingDirectoryName}/Makefile` foundSource="$foundSource $shadowFilePath" - rm -f assignment/GNUmakefile - rm -f assignment/makefile + rm -f ${studentParentWorkingDirectoryName}/GNUmakefile + rm -f ${studentParentWorkingDirectoryName}/makefile - cp -f tests/Makefile assignment/Makefile || exit 2 - sed -i "s~\bINCLUDEDIRS\s*=.*~${foundIncludeDirs}~; s~\bSOURCE\s*=.*~${foundSource}~" assignment/Makefile + cp -f ${testWorkingDirectory}/Makefile ${studentParentWorkingDirectoryName}/Makefile || exit 2 + sed -i "s~\bINCLUDEDIRS\s*=.*~${foundIncludeDirs}~; s~\bSOURCE\s*=.*~${foundSource}~" ${studentParentWorkingDirectoryName}/Makefile runAlways: false - name: build_and_run_all_tests script: |- @@ -55,10 +55,10 @@ actions: # Build and run all tests if the compilation succeeds # ------------------------------ sudo chown artemis_user:artemis_user . - gcc -c -Wall assignment/*.c || error=true + gcc -c -Wall ${studentParentWorkingDirectoryName}/*.c || error=true if [ ! $error ] then - cd tests || exit 0 + cd ${testWorkingDirectory} || exit 0 python3 Tests.py || true fi runAlways: false diff --git a/src/main/resources/templates/aeolus/c/gcc_static.sh b/src/main/resources/templates/aeolus/c/gcc_static.sh index 96847a3bbc92..2dfa84c2a569 100644 --- a/src/main/resources/templates/aeolus/c/gcc_static.sh +++ b/src/main/resources/templates/aeolus/c/gcc_static.sh @@ -10,13 +10,13 @@ setup_the_build_environment () { # Build and run all tests # ------------------------------ - # Updating assignment and test-reports ownership... - sudo chown artemis_user:artemis_user assignment/ -R + # Updating ${studentParentWorkingDirectoryName} and test-reports ownership... + sudo chown artemis_user:artemis_user ${studentParentWorkingDirectoryName}/ -R mkdir test-reports chown artemis_user:artemis_user test-reports/ -R # assignment - cd tests + cd ${testWorkingDirectory} REQ_FILE=requirements.txt if [ -f "$REQ_FILE" ]; then pip3 install --user -r requirements.txt || true @@ -35,18 +35,18 @@ setup_makefile () { # Setup makefile # ------------------------------ - shadowFilePath="../tests/testUtils/c/shadow_exec.c" + shadowFilePath="../${testWorkingDirectory}/testUtils/c/shadow_exec.c" - foundIncludeDirs=`grep -m 1 'INCLUDEDIRS\s*=' assignment/Makefile` + foundIncludeDirs=`grep -m 1 'INCLUDEDIRS\s*=' ${studentParentWorkingDirectoryName}/Makefile` - foundSource=`grep -m 1 'SOURCE\s*=' assignment/Makefile` + foundSource=`grep -m 1 'SOURCE\s*=' ${studentParentWorkingDirectoryName}/Makefile` foundSource="$foundSource $shadowFilePath" - rm -f assignment/GNUmakefile - rm -f assignment/makefile + rm -f ${studentParentWorkingDirectoryName}/GNUmakefile + rm -f ${studentParentWorkingDirectoryName}/makefile - cp -f tests/Makefile assignment/Makefile || exit 2 - sed -i "s~\bINCLUDEDIRS\s*=.*~${foundIncludeDirs}~; s~\bSOURCE\s*=.*~${foundSource}~" assignment/Makefile + cp -f ${testWorkingDirectory}/Makefile ${studentParentWorkingDirectoryName}/Makefile || exit 2 + sed -i "s~\bINCLUDEDIRS\s*=.*~${foundIncludeDirs}~; s~\bSOURCE\s*=.*~${foundSource}~" ${studentParentWorkingDirectoryName}/Makefile } build_and_run_all_tests () { @@ -58,10 +58,10 @@ build_and_run_all_tests () { # Build and run all tests if the compilation succeeds # ------------------------------ sudo chown artemis_user:artemis_user . - gcc -c -Wall assignment/*.c || error=true + gcc -c -Wall ${studentParentWorkingDirectoryName}/*.c || error=true if [ ! $error ] then - cd tests || exit 0 + cd ${testWorkingDirectory} || exit 0 python3 Tests.py || true else exit 1 diff --git a/src/main/resources/templates/aeolus/c/gcc_static.yaml b/src/main/resources/templates/aeolus/c/gcc_static.yaml index be9e9eb1dc2f..21c0b506f179 100644 --- a/src/main/resources/templates/aeolus/c/gcc_static.yaml +++ b/src/main/resources/templates/aeolus/c/gcc_static.yaml @@ -10,12 +10,12 @@ actions: # ------------------------------ # Updating assignment and test-reports ownership... - sudo chown artemis_user:artemis_user assignment/ -R + sudo chown artemis_user:artemis_user ${studentParentWorkingDirectoryName}/ -R mkdir test-reports chown artemis_user:artemis_user test-reports/ -R # assignment - cd tests + cd ${testWorkingDirectory} REQ_FILE=requirements.txt if [ -f "$REQ_FILE" ]; then pip3 install --user -r requirements.txt || true @@ -33,18 +33,18 @@ actions: # Setup makefile # ------------------------------ - shadowFilePath="../tests/testUtils/c/shadow_exec.c" + shadowFilePath="../${testWorkingDirectory}/testUtils/c/shadow_exec.c" - foundIncludeDirs=`grep -m 1 'INCLUDEDIRS\s*=' assignment/Makefile` + foundIncludeDirs=`grep -m 1 'INCLUDEDIRS\s*=' ${studentParentWorkingDirectoryName}/Makefile` - foundSource=`grep -m 1 'SOURCE\s*=' assignment/Makefile` + foundSource=`grep -m 1 'SOURCE\s*=' ${studentParentWorkingDirectoryName}/Makefile` foundSource="$foundSource $shadowFilePath" - rm -f assignment/GNUmakefile - rm -f assignment/makefile + rm -f ${studentParentWorkingDirectoryName}/GNUmakefile + rm -f ${studentParentWorkingDirectoryName}/makefile - cp -f tests/Makefile assignment/Makefile || exit 2 - sed -i "s~\bINCLUDEDIRS\s*=.*~${foundIncludeDirs}~; s~\bSOURCE\s*=.*~${foundSource}~" assignment/Makefile + cp -f ${testWorkingDirectory}/Makefile ${studentParentWorkingDirectoryName}/Makefile || exit 2 + sed -i "s~\bINCLUDEDIRS\s*=.*~${foundIncludeDirs}~; s~\bSOURCE\s*=.*~${foundSource}~" ${studentParentWorkingDirectoryName}/Makefile runAlways: false - name: build_and_run_all_tests script: |- @@ -55,10 +55,10 @@ actions: # Build and run all tests if the compilation succeeds # ------------------------------ sudo chown artemis_user:artemis_user . - gcc -c -Wall assignment/*.c || error=true + gcc -c -Wall ${studentParentWorkingDirectoryName}/*.c || error=true if [ ! $error ] then - cd tests || exit 0 + cd ${testWorkingDirectory} || exit 0 python3 Tests.py || true else exit 1 diff --git a/src/main/resources/templates/aeolus/java/plain_maven_blackbox.sh b/src/main/resources/templates/aeolus/java/plain_maven_blackbox.sh index 46186cf50311..190bc88c4831 100644 --- a/src/main/resources/templates/aeolus/java/plain_maven_blackbox.sh +++ b/src/main/resources/templates/aeolus/java/plain_maven_blackbox.sh @@ -9,9 +9,9 @@ build () { checkers () { echo '⚙️ executing checkers' # all java files in the assignment folder should have maximal line length 80 - pipeline-helper line-length -l 80 -s assignment/ -e java + pipeline-helper line-length -l 80 -s ${studentParentWorkingDirectoryName}/ -e java # checks that the file exists and is not empty for non gui programs - pipeline-helper file-exists assignment/Tests.txt + pipeline-helper file-exists ${studentParentWorkingDirectoryName}/Tests.txt main_checker_output=$(pipeline-helper main-method -s target/classes) diff --git a/src/main/resources/templates/aeolus/java/plain_maven_blackbox.yaml b/src/main/resources/templates/aeolus/java/plain_maven_blackbox.yaml index 2b4428a585d1..02d0ff15b7ad 100644 --- a/src/main/resources/templates/aeolus/java/plain_maven_blackbox.yaml +++ b/src/main/resources/templates/aeolus/java/plain_maven_blackbox.yaml @@ -5,9 +5,9 @@ actions: - name: checkers script: |- # all java files in the assignment folder should have maximal line length 80 - pipeline-helper line-length -l 80 -s assignment/ -e java + pipeline-helper line-length -l 80 -s ${studentParentWorkingDirectoryName}/ -e java # checks that the file exists and is not empty for non gui programs - pipeline-helper file-exists assignment/Tests.txt + pipeline-helper file-exists ${studentParentWorkingDirectoryName}/Tests.txt main_checker_output=$(pipeline-helper main-method -s target/classes) diff --git a/src/main/resources/templates/aeolus/java/plain_maven_blackbox_static.sh b/src/main/resources/templates/aeolus/java/plain_maven_blackbox_static.sh index 9a6be87561a0..3a1af9d0d213 100644 --- a/src/main/resources/templates/aeolus/java/plain_maven_blackbox_static.sh +++ b/src/main/resources/templates/aeolus/java/plain_maven_blackbox_static.sh @@ -9,9 +9,9 @@ build () { checkers () { echo '⚙️ executing checkers' # all java files in the assignment folder should have maximal line length 80 - pipeline-helper line-length -l 80 -s assignment/ -e java + pipeline-helper line-length -l 80 -s ${studentParentWorkingDirectoryName}/ -e java # checks that the file exists and is not empty for non gui programs - pipeline-helper file-exists assignment/Tests.txt + pipeline-helper file-exists ${studentParentWorkingDirectoryName}/Tests.txt main_checker_output=$(pipeline-helper main-method -s target/classes) diff --git a/src/main/resources/templates/aeolus/java/plain_maven_blackbox_static.yaml b/src/main/resources/templates/aeolus/java/plain_maven_blackbox_static.yaml index 68c71c046d3d..7cd6a6c07773 100644 --- a/src/main/resources/templates/aeolus/java/plain_maven_blackbox_static.yaml +++ b/src/main/resources/templates/aeolus/java/plain_maven_blackbox_static.yaml @@ -5,9 +5,9 @@ actions: - name: checkers script: |- # all java files in the assignment folder should have maximal line length 80 - pipeline-helper line-length -l 80 -s assignment/ -e java + pipeline-helper line-length -l 80 -s ${studentParentWorkingDirectoryName}/ -e java # checks that the file exists and is not empty for non gui programs - pipeline-helper file-exists assignment/Tests.txt + pipeline-helper file-exists ${studentParentWorkingDirectoryName}/Tests.txt main_checker_output=$(pipeline-helper main-method -s target/classes) diff --git a/src/main/resources/templates/aeolus/ocaml/default.sh b/src/main/resources/templates/aeolus/ocaml/default.sh index ac1cf8b65b8d..f5b5f595b38d 100644 --- a/src/main/resources/templates/aeolus/ocaml/default.sh +++ b/src/main/resources/templates/aeolus/ocaml/default.sh @@ -3,7 +3,7 @@ set -e export AEOLUS_INITIAL_DIRECTORY=${PWD} build_and_test_the_code () { echo '⚙️ executing build_and_test_the_code' - cd "tests" + cd "${testWorkingDirectory}" # the build process is specified in `run.sh` in the test repository chmod +x run.sh ./run.sh -s diff --git a/src/main/resources/templates/aeolus/ocaml/default.yaml b/src/main/resources/templates/aeolus/ocaml/default.yaml index df5aef046d3a..70cdfcad1b9f 100644 --- a/src/main/resources/templates/aeolus/ocaml/default.yaml +++ b/src/main/resources/templates/aeolus/ocaml/default.yaml @@ -5,7 +5,7 @@ actions: # the build process is specified in `run.sh` in the test repository chmod +x run.sh ./run.sh -s - workdir: tests + workdir: ${testWorkingDirectory} runAlways: false - name: junit script: '#empty script action, just for the results' diff --git a/src/main/resources/templates/aeolus/r/default.sh b/src/main/resources/templates/aeolus/r/default.sh new file mode 100644 index 000000000000..1d0b32e87105 --- /dev/null +++ b/src/main/resources/templates/aeolus/r/default.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -e +export AEOLUS_INITIAL_DIRECTORY=${PWD} +install () { + echo '⚙️ executing install' + R CMD INSTALL assignment +} + +run_all_tests () { + echo '⚙️ executing run_all_tests' + Rscript -e 'library("testthat"); options(testthat.output_file = "junit.xml"); test_local(".", reporter = "junit")' +} + +main () { + if [[ "${1}" == "aeolus_sourcing" ]]; then + return 0 # just source to use the methods in the subshell, no execution + fi + local _script_name + _script_name=${BASH_SOURCE[0]:-$0} + cd "${AEOLUS_INITIAL_DIRECTORY}" + bash -c "source ${_script_name} aeolus_sourcing; install" + cd "${AEOLUS_INITIAL_DIRECTORY}" + bash -c "source ${_script_name} aeolus_sourcing; run_all_tests" +} + +main "${@}" diff --git a/src/main/resources/templates/aeolus/r/default.yaml b/src/main/resources/templates/aeolus/r/default.yaml new file mode 100644 index 000000000000..a41d23c6f012 --- /dev/null +++ b/src/main/resources/templates/aeolus/r/default.yaml @@ -0,0 +1,14 @@ +api: v0.0.1 +metadata: + name: R + id: r + description: Test package using testthat +actions: + - name: install + script: R CMD INSTALL assignment + - name: run_all_tests + script: Rscript -e 'library("testthat"); options(testthat.output_file = "junit.xml"); test_local(".", reporter = "junit")' + results: + - name: junit + path: tests/testthat/junit.xml + type: junit diff --git a/src/main/resources/templates/aeolus/swift/plain.sh b/src/main/resources/templates/aeolus/swift/plain.sh index 2673e861d564..71387b392783 100644 --- a/src/main/resources/templates/aeolus/swift/plain.sh +++ b/src/main/resources/templates/aeolus/swift/plain.sh @@ -4,12 +4,12 @@ set -e build_and_test_the_code () { echo '⚙️ executing build_and_test_the_code' # copy test files - cp -R Tests assignment - cp Package.swift assignment + cp -R Tests ${studentParentWorkingDirectoryName} + cp Package.swift ${studentParentWorkingDirectoryName} - # In order to get the correct console output we need to execute the command within the assignment directory + # In order to get the correct console output we need to execute the command within the ${studentParentWorkingDirectoryName} directory # swift build - cd assignment + cd ${studentParentWorkingDirectoryName} swift build || error=true if [ ! $error ] diff --git a/src/main/resources/templates/aeolus/swift/plain.yaml b/src/main/resources/templates/aeolus/swift/plain.yaml index c90994ea4c4a..48211dee715a 100644 --- a/src/main/resources/templates/aeolus/swift/plain.yaml +++ b/src/main/resources/templates/aeolus/swift/plain.yaml @@ -3,12 +3,12 @@ actions: - name: build_and_test_the_code script: |- # copy test files - cp -R Tests assignment - cp Package.swift assignment + cp -R Tests ${studentParentWorkingDirectoryName} + cp Package.swift ${studentParentWorkingDirectoryName} # In order to get the correct console output we need to execute the command within the assignment directory # swift build - cd assignment + cd ${studentParentWorkingDirectoryName} swift build || error=true if [ ! $error ] @@ -24,6 +24,6 @@ actions: runAlways: false results: - name: junit_tests.xml - path: assignment/tests.xml + path: ${studentParentWorkingDirectoryName}/tests.xml type: junit before: true diff --git a/src/main/resources/templates/aeolus/swift/plain_static.sh b/src/main/resources/templates/aeolus/swift/plain_static.sh index 835494ff5454..3bbaa9fe0662 100644 --- a/src/main/resources/templates/aeolus/swift/plain_static.sh +++ b/src/main/resources/templates/aeolus/swift/plain_static.sh @@ -3,14 +3,14 @@ set -e export AEOLUS_INITIAL_DIRECTORY=${PWD} build_and_test_the_code () { echo '⚙️ executing build_and_test_the_code' - cp -R Sources assignment + cp -R Sources ${studentParentWorkingDirectoryName} # copy test files - cp -R Tests assignment - cp Package.swift assignment + cp -R Tests ${studentParentWorkingDirectoryName} + cp Package.swift ${studentParentWorkingDirectoryName} - # In order to get the correct console output we need to execute the command within the assignment directory + # In order to get the correct console output we need to execute the command within the ${studentParentWorkingDirectoryName} directory # swift build - cd assignment + cd ${studentParentWorkingDirectoryName} swift build || error=true if [ ! $error ] @@ -28,10 +28,10 @@ build_and_test_the_code () { run_static_code_analysis () { echo '⚙️ executing run_static_code_analysis' # Copy SwiftLint rules - cp .swiftlint.yml assignment || true + cp .swiftlint.yml ${studentParentWorkingDirectoryName} || true # create target directory for SCA Parser mkdir target - cd assignment + cd ${studentParentWorkingDirectoryName} # Execute static code analysis swiftlint > ../target/swiftlint-result.xml } diff --git a/src/main/resources/templates/aeolus/swift/plain_static.yaml b/src/main/resources/templates/aeolus/swift/plain_static.yaml index c900edc82d44..72f683141903 100644 --- a/src/main/resources/templates/aeolus/swift/plain_static.yaml +++ b/src/main/resources/templates/aeolus/swift/plain_static.yaml @@ -2,14 +2,14 @@ api: v0.0.1 actions: - name: build_and_test_the_code script: |- - cp -R Sources assignment + cp -R Sources ${studentParentWorkingDirectoryName} # copy test files - cp -R Tests assignment - cp Package.swift assignment + cp -R Tests ${studentParentWorkingDirectoryName} + cp Package.swift ${studentParentWorkingDirectoryName} # In order to get the correct console output we need to execute the command within the assignment directory # swift build - cd assignment + cd ${studentParentWorkingDirectoryName} swift build || error=true if [ ! $error ] @@ -26,10 +26,10 @@ actions: - name: run_static_code_analysis script: |- # Copy SwiftLint rules - cp .swiftlint.yml assignment || true + cp .swiftlint.yml ${studentParentWorkingDirectoryName} || true # create target directory for SCA Parser mkdir target - cd assignment + cd ${studentParentWorkingDirectoryName} # Execute static code analysis swiftlint > ../target/swiftlint-result.xml runAlways: true @@ -39,6 +39,6 @@ actions: before: false type: static-code-analysis - name: junit_tests.xml - path: assignment/tests.xml + path: ${studentParentWorkingDirectoryName}/tests.xml type: junit before: true diff --git a/src/main/resources/templates/aeolus/vhdl/default.sh b/src/main/resources/templates/aeolus/vhdl/default.sh index f4f5a3f3f609..747c1b20c58f 100644 --- a/src/main/resources/templates/aeolus/vhdl/default.sh +++ b/src/main/resources/templates/aeolus/vhdl/default.sh @@ -9,16 +9,16 @@ provide_environment_information () { pip3 --version echo "--------------------Contents of tests repository--------------------" - ls -la tests + ls -la ${testWorkingDirectory} echo "---------------------------------------------" echo "--------------------Contents of assignment repository--------------------" - ls -la assignment + ls -la ${studentParentWorkingDirectoryName} echo "---------------------------------------------" #Fallback in case Docker does not work as intended - REQ_FILE=tests/requirements.txt + REQ_FILE=${testWorkingDirectory}/requirements.txt if [ -f "$REQ_FILE" ]; then - pip3 install --user -r tests/requirements.txt || true + pip3 install --user -r ${testWorkingDirectory}/requirements.txt || true else echo "$REQ_FILE does not exist" fi @@ -26,16 +26,16 @@ provide_environment_information () { prepare_makefile () { echo '⚙️ executing prepare_makefile' - rm -f assignment/{GNUmakefile, Makefile, makefile} - cp -f tests/Makefile assignment/Makefile || exit 2 + rm -f ${studentParentWorkingDirectoryName}/{GNUmakefile, Makefile, makefile} + cp -f ${testWorkingDirectory}/Makefile ${studentParentWorkingDirectoryName}/Makefile || exit 2 } run_and_compile () { echo '⚙️ executing run_and_compile' - cd "tests" - python3 compileTest.py ../assignment/ + cd "${testWorkingDirectory}" + python3 compileTest.py ../${studentParentWorkingDirectoryName}/ rm compileTest.py - cp result.xml ../assignment/result.xml + cp result.xml ../${studentParentWorkingDirectoryName}/result.xml } junit () { diff --git a/src/main/resources/templates/aeolus/vhdl/default.yaml b/src/main/resources/templates/aeolus/vhdl/default.yaml index 872f94622916..64bcb4365fc5 100644 --- a/src/main/resources/templates/aeolus/vhdl/default.yaml +++ b/src/main/resources/templates/aeolus/vhdl/default.yaml @@ -8,37 +8,37 @@ actions: pip3 --version echo "--------------------Contents of tests repository--------------------" - ls -la tests + ls -la ${testWorkingDirectory} echo "---------------------------------------------" echo "--------------------Contents of assignment repository--------------------" - ls -la assignment + ls -la ${studentParentWorkingDirectoryName} echo "---------------------------------------------" #Fallback in case Docker does not work as intended - REQ_FILE=tests/requirements.txt + REQ_FILE=${testWorkingDirectory}/requirements.txt if [ -f "$REQ_FILE" ]; then - pip3 install --user -r tests/requirements.txt || true + pip3 install --user -r ${testWorkingDirectory}/requirements.txt || true else echo "$REQ_FILE does not exist" fi runAlways: false - name: prepare_makefile script: |- - rm -f assignment/{GNUmakefile, Makefile, makefile} - cp -f tests/Makefile assignment/Makefile || exit 2 + rm -f ${studentParentWorkingDirectoryName}/{GNUmakefile, Makefile, makefile} + cp -f ${testWorkingDirectory}/Makefile ${studentParentWorkingDirectoryName}/Makefile || exit 2 runAlways: false - name: run_and_compile script: |- - python3 compileTest.py ../assignment/ + python3 compileTest.py ../${studentParentWorkingDirectoryName}/ rm compileTest.py - cp result.xml ../assignment/result.xml - workdir: tests + cp result.xml ../${studentParentWorkingDirectoryName}/result.xml + workdir: ${testWorkingDirectory} runAlways: false - name: junit script: '#empty script action, just for the results' runAlways: true results: - name: assignment_junit_results - path: assignment/result.xml + path: ${studentParentWorkingDirectoryName}/result.xml type: junit before: true diff --git a/src/main/resources/templates/haskell/test/.gitignore b/src/main/resources/templates/haskell/test/.gitignore index 38ce398dd22a..39312aeed502 100755 --- a/src/main/resources/templates/haskell/test/.gitignore +++ b/src/main/resources/templates/haskell/test/.gitignore @@ -3,10 +3,10 @@ test-reports/ # Subdirectories containing other repositories template/ -solution/ +${solutionWorkingDirectory}/ # Subdirectories with test submission -assignment/ +${studentParentWorkingDirectoryName}/ # Taken from https://github.com/github/gitignore diff --git a/src/main/resources/templates/haskell/test/readme.md b/src/main/resources/templates/haskell/test/readme.md index 7ab5dcefbf25..45ac2724c923 100644 --- a/src/main/resources/templates/haskell/test/readme.md +++ b/src/main/resources/templates/haskell/test/readme.md @@ -6,8 +6,8 @@ Tests are run using [stack](https://docs.haskellstack.org/en/stable/README/) in ## Setup -The executables specified in `test.cabal` expect the solution repository checked out in the `solution` subdirectory and -the submission checked out in the `assignment` subdirectory. +The executables specified in `test.cabal` expect the solution repository checked out in the `${solutionWorkingDirectory}` subdirectory and +the submission checked out in the `${studentParentWorkingDirectoryName}` subdirectory. Moreover, `test.cabal` provides an executable to test the template repository locally. For this, it expects the template repository in the `template` subdirectory. diff --git a/src/main/resources/templates/haskell/test/run.sh b/src/main/resources/templates/haskell/test/run.sh index 06169da575a2..562c1139cf0f 100755 --- a/src/main/resources/templates/haskell/test/run.sh +++ b/src/main/resources/templates/haskell/test/run.sh @@ -13,20 +13,20 @@ done shift $((OPTIND-1)) # check for symlinks as they might be abused to link to the sample solution -$safe && find assignment/ -type l | grep -q . && echo "Cannot build with symlinks in submission." && exit 1 +$safe && find ${studentParentWorkingDirectoryName}/ -type l | grep -q . && echo "Cannot build with symlinks in submission." && exit 1 # check for unsafe OPTIONS and OPTIONS_GHC pragma as they allow to overwrite command line arguments $safe && \ while IFS= read file; do cat $file | tr -d '\n' | grep -qim 1 "{-#[[:space:]]*options" && \ echo "Cannot build with \"{-# OPTIONS..\" pragma in source." && exit 1 -done < <(find assignment/src -type f) +done < <(find ${studentParentWorkingDirectoryName}/src -type f) # build the libraries - do not forget to set the right compilation flag (Prod) stack build --allow-different-user --flag test:Prod && \ # delete the solution and tests (so that students cannot access it) when in safe mode ($safe && \ - (rm -rf solution && rm -rf test) \ + (rm -rf ${solutionWorkingDirectory} && rm -rf test) \ ) \ # run the test executable and return 0 # Note: as a convention, a failed haskell tasty test suite returns 1, but this stops the JUnit Parser from running. diff --git a/src/main/resources/templates/haskell/test/test.cabal b/src/main/resources/templates/haskell/test/test.cabal index 7cf429c106e3..16977cf154b8 100644 --- a/src/main/resources/templates/haskell/test/test.cabal +++ b/src/main/resources/templates/haskell/test/test.cabal @@ -56,7 +56,7 @@ library submission -- by setting it to a non-existent program called `nonExistentCPP`. See -- https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/phases.html ghc-options: -fpackage-trust -trust base -pgmP nonExistentCPP - hs-source-dirs: assignment/src + hs-source-dirs: ${studentParentWorkingDirectoryName}/src exposed-modules: Exercise -- build the local template @@ -74,7 +74,7 @@ library template -- build the solution library solution import: common-all - hs-source-dirs: solution/src + hs-source-dirs: ${solutionWorkingDirectory}/src exposed-modules: Exercise -- run tests for a submission diff --git a/src/main/resources/templates/java/test/blackbox/projectTemplate/.gitignore b/src/main/resources/templates/java/test/blackbox/projectTemplate/.gitignore index 44c742ee253c..2bb4fca67c4f 100644 --- a/src/main/resources/templates/java/test/blackbox/projectTemplate/.gitignore +++ b/src/main/resources/templates/java/test/blackbox/projectTemplate/.gitignore @@ -1,4 +1,4 @@ -assignment/ +${studentParentWorkingDirectoryName}/ # Taken from https://github.com/github/gitignore diff --git a/src/main/resources/templates/java/test/gradle/projectTemplate/.gitignore b/src/main/resources/templates/java/test/gradle/projectTemplate/.gitignore index bde9056a0f14..964ec2319a13 100644 --- a/src/main/resources/templates/java/test/gradle/projectTemplate/.gitignore +++ b/src/main/resources/templates/java/test/gradle/projectTemplate/.gitignore @@ -1,5 +1,5 @@ .gradle -assignment/ +${studentParentWorkingDirectoryName}/ **/build/ !src/**/build/ target/ diff --git a/src/main/resources/templates/java/test/gradle/projectTemplate/build.gradle b/src/main/resources/templates/java/test/gradle/projectTemplate/build.gradle index c6d2e5f6689a..97be1ab67bfd 100644 --- a/src/main/resources/templates/java/test/gradle/projectTemplate/build.gradle +++ b/src/main/resources/templates/java/test/gradle/projectTemplate/build.gradle @@ -28,7 +28,7 @@ dependencies { // testImplementation(':${exerciseNamePomXml}-Solution') } -def assignmentSrcDir = "assignment/src" +def assignmentSrcDir = "${studentWorkingDirectoryNoSlash}" def studentOutputDir = sourceSets.main.java.destinationDirectory.get() // %static-code-analysis-start% def scaConfigDirectory = "$projectDir/staticCodeAnalysisConfig" diff --git a/src/main/resources/templates/java/test/maven/projectTemplate/.gitignore b/src/main/resources/templates/java/test/maven/projectTemplate/.gitignore index dd177405a0d3..dbf843d6c857 100644 --- a/src/main/resources/templates/java/test/maven/projectTemplate/.gitignore +++ b/src/main/resources/templates/java/test/maven/projectTemplate/.gitignore @@ -1,4 +1,4 @@ -assignment/ +${studentParentWorkingDirectoryName}/ target/ # Taken from https://github.com/github/gitignore diff --git a/src/main/resources/templates/java/test/stagePom.xml b/src/main/resources/templates/java/test/stagePom.xml index fea2492362ef..18cfa54fc39f 100644 --- a/src/main/resources/templates/java/test/stagePom.xml +++ b/src/main/resources/templates/java/test/stagePom.xml @@ -8,7 +8,7 @@ 4.0.0 ${exerciseNamePomXml}-Tests - ${project.basedir}/../assignment/src + ${project.basedir}/..${studentWorkingDirectory} org.apache.maven.plugins diff --git a/src/main/resources/templates/javascript/test/.gitignore b/src/main/resources/templates/javascript/test/.gitignore index d81d793eaed4..0d3cf4874427 100644 --- a/src/main/resources/templates/javascript/test/.gitignore +++ b/src/main/resources/templates/javascript/test/.gitignore @@ -1,4 +1,4 @@ node_modules/ -/assignment +/${studentParentWorkingDirectoryName} /junit.xml diff --git a/src/main/resources/templates/javascript/test/package-lock.json b/src/main/resources/templates/javascript/test/package-lock.json index b18c57c3b694..79830909997b 100644 --- a/src/main/resources/templates/javascript/test/package-lock.json +++ b/src/main/resources/templates/javascript/test/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "artemis-test", "workspaces": [ - "assignment" + "${studentParentWorkingDirectoryName}" ], "devDependencies": { "@babel/core": "^7.24.7", @@ -17,7 +17,7 @@ "jest-junit": "^16.0.0" } }, - "assignment": { + "${studentParentWorkingDirectoryName}": { "name": "artemis-exercise" }, "node_modules/@ampproject/remapping": { @@ -2499,7 +2499,7 @@ } }, "node_modules/artemis-exercise": { - "resolved": "assignment", + "resolved": "${studentParentWorkingDirectoryName}", "link": true }, "node_modules/babel-jest": { diff --git a/src/main/resources/templates/javascript/test/package.json b/src/main/resources/templates/javascript/test/package.json index 3971d2b0f3c6..782e6c431492 100644 --- a/src/main/resources/templates/javascript/test/package.json +++ b/src/main/resources/templates/javascript/test/package.json @@ -6,7 +6,7 @@ "test:ci": "jest --ci --reporters=default --reporters=jest-junit" }, "workspaces": [ - "assignment" + "${studentParentWorkingDirectoryName}" ], "devDependencies": { "@babel/core": "^7.24.7", diff --git a/src/main/resources/templates/jenkins/r/regularRuns/pipeline.groovy b/src/main/resources/templates/jenkins/r/regularRuns/pipeline.groovy new file mode 100644 index 000000000000..9a2ec97b5843 --- /dev/null +++ b/src/main/resources/templates/jenkins/r/regularRuns/pipeline.groovy @@ -0,0 +1,59 @@ +/* + * 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 ''' + R CMD INSTALL assignment + Rscript -e 'library("testthat"); options(testthat.output_file = "junit.xml"); test_local(".", reporter = "junit")' + ''' + } +} + +/** + * Script of the post build tasks aggregating all JUnit files in $WORKSPACE/results. + * + * Called by Jenkins. + */ +void postBuildTasks() { + sh ''' + rm -rf results + mkdir results + if [ -e tests/testthat/junit.xml ] + then + sed -i 's/]*>//g ; s/<\\/testsuites>/<\\/testsuite>/g' tests/testthat/junit.xml + fi + cp tests/testthat/junit.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/kotlin/test/maven/projectTemplate/.gitignore b/src/main/resources/templates/kotlin/test/maven/projectTemplate/.gitignore index bff99bd68dc5..5c7928c0bc37 100644 --- a/src/main/resources/templates/kotlin/test/maven/projectTemplate/.gitignore +++ b/src/main/resources/templates/kotlin/test/maven/projectTemplate/.gitignore @@ -1,4 +1,4 @@ -assignment/ +${studentParentWorkingDirectoryName}/ target/ # Taken from https://github.com/github/gitignore diff --git a/src/main/resources/templates/kotlin/test/stagePom.xml b/src/main/resources/templates/kotlin/test/stagePom.xml index 89bcbda7d484..d525536c92f6 100644 --- a/src/main/resources/templates/kotlin/test/stagePom.xml +++ b/src/main/resources/templates/kotlin/test/stagePom.xml @@ -8,7 +8,7 @@ 4.0.0 ${exerciseNamePomXml}-Tests - ${project.basedir}/../assignment/src + ${project.basedir}/..${studentWorkingDirectory} ${project.basedir}/test diff --git a/src/main/resources/templates/ocaml/test/.gitignore b/src/main/resources/templates/ocaml/test/.gitignore index e477e6e6a9b6..fdad97ced9bf 100644 --- a/src/main/resources/templates/ocaml/test/.gitignore +++ b/src/main/resources/templates/ocaml/test/.gitignore @@ -1,6 +1,6 @@ # Things generated by the test framework -/solution/*.ml -/assignment/*.ml +/${solutionWorkingDirectory}/*.ml +/${studentParentWorkingDirectoryName}/*.ml /test/runHidden.ml /checker/checker.exe diff --git a/src/main/resources/templates/ocaml/test/checker/checker.ml b/src/main/resources/templates/ocaml/test/checker/checker.ml index b13edbad4032..daa6d48b56fa 100644 --- a/src/main/resources/templates/ocaml/test/checker/checker.ml +++ b/src/main/resources/templates/ocaml/test/checker/checker.ml @@ -96,7 +96,7 @@ let checkFile fn = violation := true; Location.report_exception Format.err_formatter exn -let studentDir = "assignment" +let studentDir = "${studentParentWorkingDirectoryName}" (** check all student files for violations *) let _ = diff --git a/src/main/resources/templates/ocaml/test/run.sh b/src/main/resources/templates/ocaml/test/run.sh index fe73a2830c98..3dc4af3ff7cd 100755 --- a/src/main/resources/templates/ocaml/test/run.sh +++ b/src/main/resources/templates/ocaml/test/run.sh @@ -3,6 +3,7 @@ # copy code from the assignment or solution to the appropriate test folder cp_code() { + mv "$2" "$1" cd "$1" || exit rm ./*.ml >/dev/null 2>&1 # shellcheck disable=SC2086 @@ -44,13 +45,13 @@ else fi # check for symlink is the submission -find ../assignment/ -type l | grep -q . && echo "Cannot build with symlinks in submission." && exit 0 +find ../${studentParentWorkingDirectoryName}/ -type l | grep -q . && echo "Cannot build with symlinks in submission." && exit 0 # include solution and assignment in the tests # this will only pick up *.ml files in the /src folders if other files are required for the tests this needs to be adjusted -cp_code solution -echo 'include Assignment' > solution/solution.ml -cp_code assignment +cp_code ${solutionWorkingDirectory} solution +echo 'include Assignment' > ${solutionWorkingDirectory}/solution.ml +cp_code ${studentParentWorkingDirectoryName} assignment # select if tests are run by generated source code as student toplevel code may run before the tests and be able to spoof a runtime signal echo "let runHidden = $RUN_HIDDEN" > test/runHidden.ml @@ -71,7 +72,7 @@ if ! timeout -s SIGTERM $BUILD_TIMEOUT checker/checker.exe; then fi # build the student submission # don't reference the tests or solution, so that we can show the build output to the student and not leak test / solution code -if ! timeout -s SIGTERM $BUILD_TIMEOUT dune build --force assignment; then +if ! timeout -s SIGTERM $BUILD_TIMEOUT dune build --force ${solutionWorkingDirectory}; then echo "Unable to build submission, please ensure that your code builds and matches the provided interface" >&2 exit 0 fi @@ -85,13 +86,13 @@ fi cd "$BUILD_ROOT" || exit # copy the test executable into the project root -mv -f tests/test/test.exe ./ +mv -f ${testWorkingDirectory}/test/test.exe ./ # to then delete all source code, to prevent access to it while running the code if $SAFE; then - rm -rf assignment - rm -rf solution - rm -rf tests + rm -rf ${studentParentWorkingDirectoryName} + rm -rf ${solutionWorkingDirectory} + rm -rf ${testWorkingDirectory} fi; # running the test executable without arguments to cause them to exit without actually running any tests diff --git a/src/main/resources/templates/python/test/behavior/behavior_test.py b/src/main/resources/templates/python/test/behavior/behavior_test.py index 15ad384df904..31c0491b0a9c 100644 --- a/src/main/resources/templates/python/test/behavior/behavior_test.py +++ b/src/main/resources/templates/python/test/behavior/behavior_test.py @@ -1,7 +1,7 @@ import unittest -from assignment.sorting_algorithms import * -from assignment.context import Context -from assignment.policy import Policy +from ${studentParentWorkingDirectoryName}.sorting_algorithms import * +from ${studentParentWorkingDirectoryName}.context import Context +from ${studentParentWorkingDirectoryName}.policy import Policy class TestSortingBehavior(unittest.TestCase): diff --git a/src/main/resources/templates/python/test/structural/structural_test.py b/src/main/resources/templates/python/test/structural/structural_test.py index 24ed0f758476..96c30edd118e 100644 --- a/src/main/resources/templates/python/test/structural/structural_test.py +++ b/src/main/resources/templates/python/test/structural/structural_test.py @@ -1,8 +1,8 @@ import unittest -from assignment import sorting_algorithms -from assignment import sort_strategy -from assignment import context -from assignment import policy +from ${studentParentWorkingDirectoryName} import sorting_algorithms +from ${studentParentWorkingDirectoryName} import sort_strategy +from ${studentParentWorkingDirectoryName} import context +from ${studentParentWorkingDirectoryName} import policy from structural import structural_helpers diff --git a/src/main/resources/templates/r/exercise/DESCRIPTION b/src/main/resources/templates/r/exercise/DESCRIPTION new file mode 100644 index 000000000000..2933cb767621 --- /dev/null +++ b/src/main/resources/templates/r/exercise/DESCRIPTION @@ -0,0 +1,7 @@ +Package: assignment +Title: Artemis R Student Assignment +Version: 0.0.0.9000 +Author: Artemis +Description: This is an assignment to be solved by students. +License: MIT +Encoding: UTF-8 diff --git a/src/main/resources/templates/r/exercise/NAMESPACE b/src/main/resources/templates/r/exercise/NAMESPACE new file mode 100644 index 000000000000..9c9f9ac2d917 --- /dev/null +++ b/src/main/resources/templates/r/exercise/NAMESPACE @@ -0,0 +1 @@ +exportPattern("^[^\\.]") diff --git a/src/main/resources/templates/r/exercise/R/convert.R b/src/main/resources/templates/r/exercise/R/convert.R new file mode 100644 index 000000000000..28e787cf2967 --- /dev/null +++ b/src/main/resources/templates/r/exercise/R/convert.R @@ -0,0 +1,3 @@ +matrix_to_column_list <- function(mat) { + # TODO: implement +} diff --git a/src/main/resources/templates/r/readme b/src/main/resources/templates/r/readme new file mode 100644 index 000000000000..73377139d293 --- /dev/null +++ b/src/main/resources/templates/r/readme @@ -0,0 +1,6 @@ +# Matrix Columns + +Write a function `matrix_to_column_list` in R that takes a matrix of any shape and converts it into a list of +column-vectors. Each element of the list should represent a column of the matrix. + +1. [task][Convert to column-vectors](converts_3x3_matrix_to_vectors,converts_4x2_matrix_to_vectors,converts_1x5_matrix_to_scalars,converts_5x1_matrix_to_vector) diff --git a/src/main/resources/templates/r/solution/DESCRIPTION b/src/main/resources/templates/r/solution/DESCRIPTION new file mode 100644 index 000000000000..2933cb767621 --- /dev/null +++ b/src/main/resources/templates/r/solution/DESCRIPTION @@ -0,0 +1,7 @@ +Package: assignment +Title: Artemis R Student Assignment +Version: 0.0.0.9000 +Author: Artemis +Description: This is an assignment to be solved by students. +License: MIT +Encoding: UTF-8 diff --git a/src/main/resources/templates/r/solution/NAMESPACE b/src/main/resources/templates/r/solution/NAMESPACE new file mode 100644 index 000000000000..9c9f9ac2d917 --- /dev/null +++ b/src/main/resources/templates/r/solution/NAMESPACE @@ -0,0 +1 @@ +exportPattern("^[^\\.]") diff --git a/src/main/resources/templates/r/solution/R/convert.R b/src/main/resources/templates/r/solution/R/convert.R new file mode 100644 index 000000000000..7d701772ab7b --- /dev/null +++ b/src/main/resources/templates/r/solution/R/convert.R @@ -0,0 +1,17 @@ +matrix_to_column_list <- function(mat) { + if (!is.matrix(mat)) { + stop("Input must be a matrix") + } + + n_cols <- ncol(mat) + + # Initialize an empty list to store column-vectors + column_list <- vector("list", length = n_cols) + + # Loop through each column and store it in the list + for (i in 1:n_cols) { + column_list[[i]] <- mat[, i] + } + + return(column_list) +} diff --git a/src/main/resources/templates/r/test/DESCRIPTION b/src/main/resources/templates/r/test/DESCRIPTION new file mode 100644 index 000000000000..e19a2b735419 --- /dev/null +++ b/src/main/resources/templates/r/test/DESCRIPTION @@ -0,0 +1,14 @@ +Package: test +Title: Artemis R Tests +Version: 0.0.0.9000 +Author: Artemis +Description: This package tests the student assignment. +License: MIT +Encoding: UTF-8 +Imports: + assignment +Remotes: + local::./assignment +Suggests: + testthat (>= 3.0.0) +Config/testthat/edition: 3 diff --git a/src/main/resources/templates/r/test/tests/testthat.R b/src/main/resources/templates/r/test/tests/testthat.R new file mode 100644 index 000000000000..388438828173 --- /dev/null +++ b/src/main/resources/templates/r/test/tests/testthat.R @@ -0,0 +1,12 @@ +# This file is part of the standard setup for testthat. +# It is recommended that you do not modify it. +# +# Where should you do additional test configuration? +# Learn more about the roles of various files in: +# * https://r-pkgs.org/testing-design.html#sec-tests-files-overview +# * https://testthat.r-lib.org/articles/special-files.html + +library(testthat) +library(tests) + +test_check("tests") diff --git a/src/main/resources/templates/r/test/tests/testthat/test-convert.R b/src/main/resources/templates/r/test/tests/testthat/test-convert.R new file mode 100644 index 000000000000..a84a0e879711 --- /dev/null +++ b/src/main/resources/templates/r/test/tests/testthat/test-convert.R @@ -0,0 +1,47 @@ +test_that("converts_3x3_matrix_to_vectors", { + mat <- matrix(c(5, 8, 11, 6, 9, 12, 7, 10, 13), nrow = 3, ncol = 3) + + result <- assignment::matrix_to_column_list(mat) + + # Make sure to only use exactly one "expect_" function per test + expect_equal(result, list( + c(5, 8, 11), + c(6, 9, 12), + c(7, 10, 13) + )) +}) + +test_that("converts_4x2_matrix_to_vectors", { + mat <- matrix(c(13, 13, 5, 18, 11, 4, 7, 10), nrow = 4, ncol = 2) + + result <- assignment::matrix_to_column_list(mat) + + expect_equal(result, list( + c(13, 13, 5, 18), + c(11, 4, 7, 10) + )) +}) + +test_that("converts_1x5_matrix_to_scalars", { + mat <- matrix(c(16, 10, 15, 8, 7), nrow = 1, ncol = 5) + + result <- assignment::matrix_to_column_list(mat) + + expect_equal(result, list( + 16, + 10, + 15, + 8, + 7 + )) +}) + +test_that("converts_5x1_matrix_to_vector", { + mat <- matrix(c(14, 9, 1, 3, 4), nrow = 5, ncol = 1) + + result <- assignment::matrix_to_column_list(mat) + + expect_equal(result, list( + c(14, 9, 1, 3, 4) + )) +}) diff --git a/src/main/resources/templates/rust/test/.gitignore b/src/main/resources/templates/rust/test/.gitignore index a37c5236a444..aecf1e1c91fa 100644 --- a/src/main/resources/templates/rust/test/.gitignore +++ b/src/main/resources/templates/rust/test/.gitignore @@ -1,2 +1,2 @@ /target -/assignment +/${studentParentWorkingDirectoryName} diff --git a/src/main/resources/templates/rust/test/Cargo.toml b/src/main/resources/templates/rust/test/Cargo.toml index 07f82b3f09f0..f7f39b905be0 100644 --- a/src/main/resources/templates/rust/test/Cargo.toml +++ b/src/main/resources/templates/rust/test/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] chrono = { version = "0.4.38", default-features = false } -rust-template-exercise = { path = "assignment" } +rust-template-exercise = { path = "${studentParentWorkingDirectoryName}" } syn = { version = "2.0.72", features = ["full"] } rust_template_test_macros = { path = "./rust_template_test_macros" } diff --git a/src/main/resources/templates/rust/test/build.rs b/src/main/resources/templates/rust/test/build.rs index 850fcb846cce..947f4783dbd7 100644 --- a/src/main/resources/templates/rust/test/build.rs +++ b/src/main/resources/templates/rust/test/build.rs @@ -6,7 +6,7 @@ use std::{fs, io}; use syn::{parse_file, FnArg, ImplItem, Item, TraitItem, Type, TypeParamBound}; -const SRC_DIR: &str = "assignment/src"; +const SRC_DIR: &str = "${studentWorkingDirectoryNoSlash}"; fn main() { println!("cargo::rerun-if-changed={SRC_DIR}"); diff --git a/src/main/resources/templates/rust/test/tests/structural.rs b/src/main/resources/templates/rust/test/tests/structural.rs index 70b0d19c9131..cf0bef375605 100644 --- a/src/main/resources/templates/rust/test/tests/structural.rs +++ b/src/main/resources/templates/rust/test/tests/structural.rs @@ -4,14 +4,14 @@ use structural_helpers::*; #[test] fn test_sort_strategy_trait() { - let ast = parse_file("./assignment/src/sort_strategy.rs"); + let ast = parse_file(".${studentWorkingDirectory}/sort_strategy.rs"); check_trait_names(&ast.items, ["SortStrategy"]) .unwrap_or_else(|name| panic!("A trait named \"{name}\" should be defined")); } #[test] fn test_sort_strategy_supertrait() { - let ast = parse_file("./assignment/src/sort_strategy.rs"); + let ast = parse_file(".${studentWorkingDirectory}/sort_strategy.rs"); let sort_strategy = find_trait(&ast.items, "SortStrategy") .expect("A trait named \"SortStrategy\" should be defined"); check_trait_supertrait(sort_strategy, "Any") @@ -20,7 +20,7 @@ fn test_sort_strategy_supertrait() { #[test] fn test_sort_strategy_methods() { - let ast = parse_file("./assignment/src/sort_strategy.rs"); + let ast = parse_file(".${studentWorkingDirectory}/sort_strategy.rs"); let sort_strategy = find_trait(&ast.items, "SortStrategy") .expect("A trait named \"SortStrategy\" should be defined"); check_trait_function_names(&sort_strategy.items, ["perform_sort"]) @@ -29,7 +29,7 @@ fn test_sort_strategy_methods() { #[test] fn test_context_fields() { - let ast = parse_file("./assignment/src/context.rs"); + let ast = parse_file(".${studentWorkingDirectory}/context.rs"); let context = find_struct(&ast.items, "Context").expect("A struct named \"Context\" should be defined"); check_struct_field_names(&context.fields, ["sort_algorithm"]) @@ -38,7 +38,7 @@ fn test_context_fields() { #[test] fn test_context_methods() { - let ast = parse_file("./assignment/src/context.rs"); + let ast = parse_file(".${studentWorkingDirectory}/context.rs"); let context_impl = find_impl(&ast.items, "Context").expect("SortStrategy should implement functions"); check_impl_function_names(&context_impl.items, ["new", "sort", "sort_algorithm"]) @@ -47,7 +47,7 @@ fn test_context_methods() { #[test] fn test_policy_fields() { - let ast = parse_file("./assignment/src/policy.rs"); + let ast = parse_file(".${studentWorkingDirectory}/policy.rs"); let policy = find_struct(&ast.items, "Policy").expect("A struct named \"Policy\" should be defined"); check_struct_field_names(&policy.fields, ["context"]) @@ -56,7 +56,7 @@ fn test_policy_fields() { #[test] fn test_policy_methods() { - let ast = parse_file("./assignment/src/policy.rs"); + let ast = parse_file(".${studentWorkingDirectory}/policy.rs"); let policy_impl = find_impl(&ast.items, "Policy").expect("Policy should implement functions"); check_impl_function_names(&policy_impl.items, ["new", "configure"]) .unwrap_or_else(|name| panic!("Policy should implement the function \"{name}\"")); @@ -64,7 +64,7 @@ fn test_policy_methods() { #[test] fn test_bubble_sort_struct() { - let ast = parse_file("./assignment/src/bubble_sort.rs"); + let ast = parse_file(".${studentWorkingDirectory}/bubble_sort.rs"); find_struct(&ast.items, "BubbleSort").expect("A struct named \"BubbleSort\" should be defined"); find_impl_for(&ast.items, "BubbleSort", "SortStrategy") .expect("BubbleSort should implement the trait \"SortStrategy\""); @@ -72,7 +72,7 @@ fn test_bubble_sort_struct() { #[test] fn test_merge_sort_struct() { - let ast = parse_file("./assignment/src/merge_sort.rs"); + let ast = parse_file("./${studentWorkingDirectory}/merge_sort.rs"); find_struct(&ast.items, "MergeSort").expect("A struct named \"MergeSort\" should be defined"); find_impl_for(&ast.items, "MergeSort", "SortStrategy") .expect("MergeSort should implement the trait \"SortStrategy\""); diff --git a/src/main/resources/templates/swift/Swift-Server-Setup.md b/src/main/resources/templates/swift/Swift-Server-Setup.md index 425f7244566c..58cc83d1a18b 100644 --- a/src/main/resources/templates/swift/Swift-Server-Setup.md +++ b/src/main/resources/templates/swift/Swift-Server-Setup.md @@ -55,7 +55,7 @@ Append following to ~/.bashrc: # Bamboo Build Plan ## Create Tasks Go to Plan Configuration > Default Job > Tasks -- Create default task to checkout repos "tests and assignment" +- Create default task to checkout repos "tests and ${studentParentWorkingDirectoryName}" - Create a task to build the swift project - Name the task `Build swift`. - Interpreter: `Shell` diff --git a/src/main/resources/templates/swift/xcode/test/${appName}Test.xcodeproj/xcshareddata/xcschemes/${appName}Test.xcscheme b/src/main/resources/templates/swift/xcode/test/${appName}Test.xcodeproj/xcshareddata/xcschemes/${appName}Test.xcscheme index fa4508ce15ce..1d83b9c00883 100644 --- a/src/main/resources/templates/swift/xcode/test/${appName}Test.xcodeproj/xcshareddata/xcschemes/${appName}Test.xcscheme +++ b/src/main/resources/templates/swift/xcode/test/${appName}Test.xcodeproj/xcshareddata/xcschemes/${appName}Test.xcscheme @@ -41,7 +41,7 @@ BlueprintIdentifier = "4F4474F426BD8F87004E6064" BuildableName = "${appName}.app" BlueprintName = "${appName}" - ReferencedContainer = "container:assignment/${appName}.xcodeproj"> + ReferencedContainer = "container:${studentParentWorkingDirectoryName}/${appName}.xcodeproj"> @@ -57,7 +57,7 @@ BlueprintIdentifier = "4F4474F426BD8F87004E6064" BuildableName = "${appName}.app" BlueprintName = "${appName}" - ReferencedContainer = "container:assignment/${appName}.xcodeproj"> + ReferencedContainer = "container:${studentParentWorkingDirectoryName}/${appName}.xcodeproj"> diff --git a/src/main/resources/templates/swift/xcode/test/${appName}Test.xcodeproj/xcshareddata/xcschemes/${appName}UITest.xcscheme b/src/main/resources/templates/swift/xcode/test/${appName}Test.xcodeproj/xcshareddata/xcschemes/${appName}UITest.xcscheme index 11353953efa7..86d626978f97 100644 --- a/src/main/resources/templates/swift/xcode/test/${appName}Test.xcodeproj/xcshareddata/xcschemes/${appName}UITest.xcscheme +++ b/src/main/resources/templates/swift/xcode/test/${appName}Test.xcodeproj/xcshareddata/xcschemes/${appName}UITest.xcscheme @@ -41,7 +41,7 @@ ${appName} BlueprintIdentifier = "4F4474F426BD8F87004E6064" BuildableName = "${appName}.app" BlueprintName = "${appName}" - ReferencedContainer = "container:assignment/${appName}.xcodeproj"> + ReferencedContainer = "container:${studentParentWorkingDirectoryName}/${appName}.xcodeproj"> @@ -57,7 +57,7 @@ ${appName} BlueprintIdentifier = "4F4474F426BD8F87004E6064" BuildableName = "${appName}.app" BlueprintName = "${appName}" - ReferencedContainer = "container:assignment/${appName}.xcodeproj"> + ReferencedContainer = "container:${studentParentWorkingDirectoryName}/${appName}.xcodeproj"> diff --git a/src/main/resources/templates/swift/xcode/test/${appName}Test.xcworkspace/contents.xcworkspacedata b/src/main/resources/templates/swift/xcode/test/${appName}Test.xcworkspace/contents.xcworkspacedata index 70fc7ea09f74..bcc06bae07c6 100644 --- a/src/main/resources/templates/swift/xcode/test/${appName}Test.xcworkspace/contents.xcworkspacedata +++ b/src/main/resources/templates/swift/xcode/test/${appName}Test.xcworkspace/contents.xcworkspacedata @@ -5,6 +5,6 @@ location = "group:${appName}Test.xcodeproj"> + location = "group:${studentParentWorkingDirectoryName}/${appName}.xcodeproj"> - \ No newline at end of file + diff --git a/src/main/resources/templates/swift/xcode/test/.swiftlint.yml b/src/main/resources/templates/swift/xcode/test/.swiftlint.yml index 5c35904fd2e7..39604f106d66 100644 --- a/src/main/resources/templates/swift/xcode/test/.swiftlint.yml +++ b/src/main/resources/templates/swift/xcode/test/.swiftlint.yml @@ -292,7 +292,7 @@ only_rules: # An XCTFail call should include a description of the assertion. included: # paths to include during linting. `--path` is ignored if present. - - assignment + - ${studentParentWorkingDirectoryName} excluded: # paths to ignore during linting. Takes precedence over `included`. - Carthage diff --git a/src/main/resources/templates/swift/xcode/test/README.md b/src/main/resources/templates/swift/xcode/test/README.md index 61991fee8ef8..d0093037df8c 100644 --- a/src/main/resources/templates/swift/xcode/test/README.md +++ b/src/main/resources/templates/swift/xcode/test/README.md @@ -1,7 +1,7 @@ This is the combined repo that will be produced on the build agent by cloning two repos -1) exercise --> everything in the assignment folder -2) tests --> everything except the assignment folder +1) exercise --> everything in the ${studentParentWorkingDirectoryName} folder +2) tests --> everything except the ${studentParentWorkingDirectoryName} folder The tests can be executed as follows diff --git a/src/main/webapp/app/admin/metrics/metrics.model.ts b/src/main/webapp/app/admin/metrics/metrics.model.ts index 1e006405c805..dbed33af6fc7 100644 --- a/src/main/webapp/app/admin/metrics/metrics.model.ts +++ b/src/main/webapp/app/admin/metrics/metrics.model.ts @@ -84,6 +84,7 @@ export enum HttpMethod { Post = 'POST', Get = 'GET', Delete = 'DELETE', + Patch = 'PATCH', } export interface ProcessMetrics { diff --git a/src/main/webapp/app/core/auth/account.service.ts b/src/main/webapp/app/core/auth/account.service.ts index 93b66699ebba..8c22990b2a17 100644 --- a/src/main/webapp/app/core/auth/account.service.ts +++ b/src/main/webapp/app/core/auth/account.service.ts @@ -224,6 +224,10 @@ export class AccountService implements IAccountService { return this.hasAnyAuthorityDirect([Authority.ADMIN]); } + isAtLeastTutor(): boolean { + return this.hasAnyAuthorityDirect([Authority.ADMIN, Authority.EDITOR, Authority.INSTRUCTOR, Authority.TA]); + } + isAuthenticated(): boolean { return this.authenticated; } diff --git a/src/main/webapp/app/core/config/monaco.config.ts b/src/main/webapp/app/core/config/monaco.config.ts index aa40e47c177c..f37dfe5a4069 100644 --- a/src/main/webapp/app/core/config/monaco.config.ts +++ b/src/main/webapp/app/core/config/monaco.config.ts @@ -1,19 +1,23 @@ /** * Sets up the MonacoEnvironment for the monaco editor's service worker. + * See https://github.com/microsoft/monaco-editor/blob/main/samples/browser-esm-esbuild/index.js */ export function MonacoConfig() { self.MonacoEnvironment = { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - getWorkerUrl: function (workerId: string, label: string) { - /* - * This is the AMD-based service worker, which comes bundled with a few special workers for selected languages. - * (e.g.: javascript, typescript, html, css) - * - * It is also possible to use an ESM-based approach, which requires a little more setup and case distinctions in this method. - * At the moment, it seems that the ESM-based approaches are incompatible with the Artemis client, as they would require custom builders. - * Support for custom builders was removed in #6546. - */ - return 'vs/base/worker/workerMain.js'; + getWorkerUrl: (_moduleId: string, label: string): string => { + if (label === 'json') { + return './vs/language/json/json.worker.js'; + } + if (label === 'css' || label === 'scss' || label === 'less') { + return './vs/language/css/css.worker.js'; + } + if (label === 'html' || label === 'handlebars' || label === 'razor') { + return './vs/language/html/html.worker.js'; + } + if (label === 'typescript' || label === 'javascript') { + return './vs/language/typescript/ts.worker.js'; + } + return './vs/editor/editor.worker.js'; }, }; } diff --git a/src/main/webapp/app/core/user/admin-user.service.ts b/src/main/webapp/app/core/user/admin-user.service.ts index 0614e9229d3b..73e14d154597 100644 --- a/src/main/webapp/app/core/user/admin-user.service.ts +++ b/src/main/webapp/app/core/user/admin-user.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; +import { HttpClient, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { createRequestOption } from 'app/shared/util/request.util'; @@ -94,11 +94,7 @@ export class AdminUserService { * @return Observable> */ deleteUsers(logins: string[]): Observable> { - let params = new HttpParams(); - for (const login of logins) { - params = params.append('login', login); - } - return this.http.delete(`${this.resourceUrl}`, { params, observe: 'response' }); + return this.http.delete(`${this.resourceUrl}`, { body: logins, observe: 'response' }); } /** diff --git a/src/main/webapp/app/core/user/user.model.ts b/src/main/webapp/app/core/user/user.model.ts index 816cf4fc9a9c..f793581b21a3 100644 --- a/src/main/webapp/app/core/user/user.model.ts +++ b/src/main/webapp/app/core/user/user.model.ts @@ -16,6 +16,7 @@ export class User extends Account { public vcsAccessToken?: string; public vcsAccessTokenExpiryDate?: string; public sshPublicKey?: string; + public sshKeyHash?: string; public irisAccepted?: dayjs.Dayjs; constructor( @@ -66,6 +67,7 @@ export class UserPublicInfoDTO { public firstName?: string; public lastName?: string; public email?: string; + public imageUrl?: string; public isInstructor?: boolean; public isEditor?: boolean; public isTeachingAssistant?: boolean; diff --git a/src/main/webapp/app/course/competencies/competency-card/competency-card.component.html b/src/main/webapp/app/course/competencies/competency-card/competency-card.component.html index ffddb7b4643a..27038983513a 100644 --- a/src/main/webapp/app/course/competencies/competency-card/competency-card.component.html +++ b/src/main/webapp/app/course/competencies/competency-card/competency-card.component.html @@ -1,19 +1,19 @@

    - @if (courseId && !isPrerequisite) { - + @if (courseId() && !isPrerequisite()) { + }
    @@ -22,45 +22,47 @@

    - +

    - {{ competency.title }} + {{ competency()?.title }} @if (isMastered) { } - @if (competency.optional) { + @if (competency()?.optional) { }

    - @if (competency.description) { -

    + @if (competency()?.description) { +

    } - @if (isPrerequisite && competency.linkedCourseCompetency?.course) { + @if (isPrerequisite() && competency()?.linkedCourseCompetency?.course) {
    - @if (competency.linkedCourseCompetency!.course!.title) { - {{ competency.linkedCourseCompetency!.course!.title! }} + @if (competency()!.linkedCourseCompetency!.course!.title) { + {{ competency()!.linkedCourseCompetency!.course!.title! }} } - @if (competency.linkedCourseCompetency!.course!.semester) { - {{ competency.linkedCourseCompetency!.course!.semester! }} + @if (competency()!.linkedCourseCompetency!.course!.semester) { + {{ competency()!.linkedCourseCompetency!.course!.semester! }} }
    }
    - @if (competency.softDueDate) { + @if (competency()?.softDueDate) {
    - {{ competency.softDueDate! | artemisTimeAgo }} + {{ competency()!.softDueDate! | artemisTimeAgo }}
    } -
    - -
    + @if (!noProgressRings()) { +
    + +
    + }
    diff --git a/src/main/webapp/app/course/competencies/competency-card/competency-card.component.ts b/src/main/webapp/app/course/competencies/competency-card/competency-card.component.ts index e7e8829c0680..462369931d12 100644 --- a/src/main/webapp/app/course/competencies/competency-card/competency-card.component.ts +++ b/src/main/webapp/app/course/competencies/competency-card/competency-card.component.ts @@ -1,7 +1,7 @@ import dayjs from 'dayjs/esm'; -import { Component, Input } from '@angular/core'; +import { Component, input } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { Competency, CompetencyProgress, getIcon, getMastery, getProgress } from 'app/entities/competency.model'; +import { CompetencyProgress, CourseCompetency, getIcon, getMastery, getProgress } from 'app/entities/competency.model'; @Component({ selector: 'jhi-competency-card', @@ -9,22 +9,20 @@ import { Competency, CompetencyProgress, getIcon, getMastery, getProgress } from styleUrls: ['../../../overview/course-exercises/course-exercise-row.scss'], }) export class CompetencyCardComponent { - @Input() - courseId: number | undefined; - @Input() - competency: Competency; - @Input() - isPrerequisite: boolean; - @Input() - hideProgress = false; + courseId = input(); + competency = input(); + isPrerequisite = input(); + hideProgress = input(false); + noProgressRings = input(false); - getIcon = getIcon; + protected readonly getIcon = getIcon; constructor(public translateService: TranslateService) {} getUserProgress(): CompetencyProgress { - if (this.competency.userProgress?.length) { - return this.competency.userProgress.first()!; + const userProgress = this.competency()?.userProgress?.first(); + if (userProgress) { + return userProgress; } return { progress: 0, confidence: 1 } as CompetencyProgress; } @@ -42,6 +40,6 @@ export class CompetencyCardComponent { } get softDueDatePassed(): boolean { - return dayjs().isAfter(this.competency.softDueDate); + return dayjs().isAfter(this.competency()?.softDueDate); } } diff --git a/src/main/webapp/app/course/competencies/competency-management/competency-management.component.ts b/src/main/webapp/app/course/competencies/competency-management/competency-management.component.ts index 76db9f9fba09..ca7ce5bdd1d7 100644 --- a/src/main/webapp/app/course/competencies/competency-management/competency-management.component.ts +++ b/src/main/webapp/app/course/competencies/competency-management/competency-management.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit, inject } from '@angular/core'; +import { Component, OnDestroy, OnInit, inject, signal } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { AlertService } from 'app/core/util/alert.service'; import { @@ -11,14 +11,11 @@ import { dtoToCompetencyRelation, getIcon, } from 'app/entities/competency.model'; -import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; -import { filter, map } from 'rxjs/operators'; import { onError } from 'app/shared/util/global.utils'; -import { Subject, Subscription, forkJoin } from 'rxjs'; +import { Subject, Subscription } from 'rxjs'; import { faFileImport, faPencilAlt, faPlus, faRobot, faTrash } from '@fortawesome/free-solid-svg-icons'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { DocumentationType } from 'app/shared/components/documentation-button/documentation-button.component'; -import { ImportAllCompetenciesComponent, ImportAllFromCourseResult } from 'app/course/competencies/competency-management/import-all-competencies.component'; import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; import { IrisSettingsService } from 'app/iris/settings/shared/iris-settings.service'; import { PROFILE_IRIS } from 'app/app.constants'; @@ -26,7 +23,11 @@ import { ConfirmAutofocusModalComponent } from 'app/shared/components/confirm-au import { TranslateService } from '@ngx-translate/core'; import { FeatureToggle, FeatureToggleService } from 'app/shared/feature-toggle/feature-toggle.service'; import { Prerequisite } from 'app/entities/prerequisite.model'; -import { CourseCompetencyService } from 'app/course/competencies/course-competency.service'; +import { + ImportAllCourseCompetenciesModalComponent, + ImportAllCourseCompetenciesResult, +} from 'app/course/competencies/components/import-all-course-competencies-modal/import-all-course-competencies-modal.component'; +import { CourseCompetencyApiService } from 'app/course/competencies/services/course-competency-api.service'; @Component({ selector: 'jhi-competency-management', @@ -60,7 +61,7 @@ export class CompetencyManagementComponent implements OnInit, OnDestroy { // Injected services private readonly activatedRoute: ActivatedRoute = inject(ActivatedRoute); - private readonly courseCompetencyService: CourseCompetencyService = inject(CourseCompetencyService); + private readonly courseCompetencyApiService: CourseCompetencyApiService = inject(CourseCompetencyApiService); private readonly alertService: AlertService = inject(AlertService); private readonly modalService: NgbModal = inject(NgbModal); private readonly profileService: ProfileService = inject(ProfileService); @@ -69,12 +70,10 @@ export class CompetencyManagementComponent implements OnInit, OnDestroy { private readonly featureToggleService: FeatureToggleService = inject(FeatureToggleService); ngOnInit(): void { - this.activatedRoute.parent!.params.subscribe((params) => { - this.courseId = params['courseId']; - if (this.courseId) { - this.loadData(); - this.loadIrisEnabled(); - } + this.activatedRoute.parent!.params.subscribe(async (params) => { + this.courseId = Number(params['courseId']); + await this.loadData(); + this.loadIrisEnabled(); }); this.standardizedCompetencySubscription = this.featureToggleService.getFeatureToggleActive(FeatureToggle.StandardizedCompetencies).subscribe((isActive) => { this.standardizedCompetenciesEnabled = isActive; @@ -107,54 +106,48 @@ export class CompetencyManagementComponent implements OnInit, OnDestroy { /** * Loads all data for the competency management: Prerequisites, competencies (with average course progress) and competency relations */ - loadData() { - this.isLoading = true; - const relationsObservable = this.courseCompetencyService.getCompetencyRelations(this.courseId); - const courseCompetenciesObservable = this.courseCompetencyService.getAllForCourse(this.courseId); - - forkJoin([relationsObservable, courseCompetenciesObservable]).subscribe({ - next: ([competencyRelations, courseCompetencies]) => { - const courseCompetenciesResponse = courseCompetencies.body ?? []; - this.competencies = courseCompetenciesResponse.filter((competency) => competency.type === CourseCompetencyType.COMPETENCY); - this.prerequisites = courseCompetenciesResponse.filter((competency) => competency.type === CourseCompetencyType.PREREQUISITE); - this.courseCompetencies = courseCompetenciesResponse; - this.relations = (competencyRelations.body ?? []).map((relationDTO) => dtoToCompetencyRelation(relationDTO)); - - this.isLoading = false; - }, - error: (errorResponse: HttpErrorResponse) => onError(this.alertService, errorResponse), - }); + async loadData() { + try { + this.isLoading = true; + this.relations = (await this.courseCompetencyApiService.getCourseCompetencyRelations(this.courseId)).map(dtoToCompetencyRelation); + this.courseCompetencies = await this.courseCompetencyApiService.getCourseCompetenciesByCourseId(this.courseId); + this.competencies = this.courseCompetencies.filter((competency) => competency.type === CourseCompetencyType.COMPETENCY); + this.prerequisites = this.courseCompetencies.filter((competency) => competency.type === CourseCompetencyType.PREREQUISITE); + } catch (error) { + onError(this.alertService, error); + } finally { + this.isLoading = false; + } } /** * Opens a modal for selecting a course to import all competencies from. */ - openImportAllModal() { - const modalRef = this.modalService.open(ImportAllCompetenciesComponent, { size: 'lg', backdrop: 'static' }); - //unary operator is necessary as otherwise courseId is seen as a string and will not match. - modalRef.componentInstance.disabledIds = [+this.courseId]; - modalRef.componentInstance.competencyType = 'courseCompetency'; - modalRef.result.then((result: ImportAllFromCourseResult) => { - const courseTitle = result.courseForImportDTO.title ?? ''; - - this.courseCompetencyService - .importAll(this.courseId, result.courseForImportDTO.id!, result.importRelations) - .pipe( - filter((res: HttpResponse>) => res.ok), - map((res: HttpResponse>) => res.body), - ) - .subscribe({ - next: (res: Array) => { - if (res.length > 0) { - this.alertService.success(`artemisApp.courseCompetency.importAll.success`, { noOfCompetencies: res.length, courseTitle: courseTitle }); - this.updateDataAfterImportAll(res); - } else { - this.alertService.warning(`artemisApp.courseCompetency.importAll.warning`, { courseTitle: courseTitle }); - } - }, - error: (res: HttpErrorResponse) => onError(this.alertService, res), - }); + async openImportAllModal() { + const modalRef = this.modalService.open(ImportAllCourseCompetenciesModalComponent, { + size: 'lg', + backdrop: 'static', }); + modalRef.componentInstance.courseId = signal(this.courseId); + const importResults: ImportAllCourseCompetenciesResult | undefined = await modalRef.result; + if (!importResults) { + return; + } + const courseTitle = importResults.course.title ?? ''; + try { + const importedCompetencies = await this.courseCompetencyApiService.importAllByCourseId(this.courseId, importResults.courseCompetencyImportOptions); + if (importedCompetencies.length) { + this.alertService.success(`artemisApp.courseCompetency.importAll.success`, { + noOfCompetencies: importedCompetencies.length, + courseTitle: courseTitle, + }); + this.updateDataAfterImportAll(importedCompetencies); + } else { + this.alertService.warning(`artemisApp.courseCompetency.importAll.warning`, { courseTitle: courseTitle }); + } + } catch (error) { + onError(this.alertService, error); + } } /** @@ -169,7 +162,7 @@ export class CompetencyManagementComponent implements OnInit, OnDestroy { .map((dto) => dto.tailRelations) .flat() .filter((element): element is CompetencyRelationDTO => !!element) - .map((dto) => dtoToCompetencyRelation(dto)); + .map(dtoToCompetencyRelation); this.competencies = this.competencies.concat(importedCompetencies); this.prerequisites = this.prerequisites.concat(importedPrerequisites); @@ -182,21 +175,17 @@ export class CompetencyManagementComponent implements OnInit, OnDestroy { * * @param relation the given competency relation */ - createRelation(relation: CompetencyRelation) { - this.courseCompetencyService - .createCompetencyRelation(relation, this.courseId) - .pipe( - filter((res) => res.ok), - map((res) => res.body), - ) - .subscribe({ - next: (relation) => { - if (relation) { - this.relations = this.relations.concat(dtoToCompetencyRelation(relation)); - } - }, - error: (res: HttpErrorResponse) => onError(this.alertService, res), + async createRelation(relation: CompetencyRelation) { + try { + const createdRelation = await this.courseCompetencyApiService.createCourseCompetencyRelation(this.courseId, { + headCompetencyId: relation.headCompetency?.id, + tailCompetencyId: relation.tailCompetency?.id, + relationType: relation.type, }); + this.relations = this.relations.concat(dtoToCompetencyRelation(createdRelation)); + } catch (error) { + onError(this.alertService, error); + } } /** @@ -225,13 +214,13 @@ export class CompetencyManagementComponent implements OnInit, OnDestroy { * * @param relationId the given id */ - private removeRelation(relationId: number) { - this.courseCompetencyService.removeCompetencyRelation(relationId, this.courseId).subscribe({ - next: () => { - this.relations = this.relations.filter((relation) => relation.id !== relationId); - }, - error: (res: HttpErrorResponse) => onError(this.alertService, res), - }); + private async removeRelation(relationId: number) { + try { + await this.courseCompetencyApiService.deleteCourseCompetencyRelation(this.courseId, relationId); + this.relations = this.relations.filter((relation) => relation.id !== relationId); + } catch (error) { + onError(this.alertService, error); + } } onRemoveCompetency(competencyId: number) { diff --git a/src/main/webapp/app/course/competencies/competency-management/competency-relation-graph.component.html b/src/main/webapp/app/course/competencies/competency-management/competency-relation-graph.component.html index 1be5b1aa75c3..c16d5c6bd2d9 100644 --- a/src/main/webapp/app/course/competencies/competency-management/competency-relation-graph.component.html +++ b/src/main/webapp/app/course/competencies/competency-management/competency-relation-graph.component.html @@ -38,6 +38,7 @@ > @for (relationType of competencyRelationType | keyvalue: keepOrder; track relationType) {
+ - -
- @if (courseActionItems?.length && !anyItemHidden) { - - } - + + + @if (course) {
-
- - +
+ @if (hasSidebar) { + + } +
{{ 'artemisApp.courseOverview.menu.' + pageTitle | artemisTranslate }}
+
+
+ @if (isNotManagementView && course.isAtLeastTutor) { +
+ +
+ } + @if (showRefreshButton) { + + }
-
-
- - - @if (course) { -
-
- @if (hasSidebar) { - - } -
{{ 'artemisApp.courseOverview.menu.' + pageTitle | artemisTranslate }}
-
-
- @if (isNotManagementView && course.isAtLeastTutor) { -
- -
- } - @if (showRefreshButton) { - +
+ @if (!hasSidebar) { + } + +
+ +
-
-
- @if (!hasSidebar) { - - } - -
- -
-
- } - - + } + + +
+ +} @else { +
+ @if (!hasSidebar) { + + } + +
+ +
- +} diff --git a/src/main/webapp/app/overview/course-overview.component.scss b/src/main/webapp/app/overview/course-overview.component.scss index b9bf10ecf997..872969c33810 100644 --- a/src/main/webapp/app/overview/course-overview.component.scss +++ b/src/main/webapp/app/overview/course-overview.component.scss @@ -2,6 +2,13 @@ $menu-width-closed: 64px; $menu-width-open: 220px; $breadcrumb-height: 45px; // needed to make the exam fullscreen +// Sidebar Button Transition Variables +$transition-delay: 0.3s; +$transition-in-between-delay: 0.2s; +$transition-chevron-rotate-length: 0.2s; +$transition-chevron-max-width-length: 0.2s; +$transition-color-length: 0.2s; + .sidebar-container { width: $menu-width-open; &.collapsed { @@ -189,12 +196,12 @@ jhi-secured-image { } .double-arrow.menu-closed { - transform: translate(16px) rotate(0deg); + transform: translate(16px); } .double-arrow { - transform: translate(180px) rotate(180deg); - transition: all ease 0.3s; + transform: translate(180px); + transition: transform ease 0.3s; cursor: pointer; width: 30px; align-items: center; @@ -202,6 +209,16 @@ jhi-secured-image { display: flex; } +.menu-closed .double-arrow-icon { + transform: rotate(0deg); +} + +.double-arrow-icon { + transform: rotate(180deg); + + transition: transform ease 0.3s 0.3s; +} + .me-negative { margin-right: -5px; } @@ -242,14 +259,87 @@ jhi-secured-image { } .btn-sidebar-collapse { - background-color: var(--link-item-bg); + position: relative; + overflow: hidden; + display: inline-flex; + align-items: center; + justify-content: start; + background-color: transparent; &:hover { - background-color: var(--sidebar-card-selected-bg); color: var(--primary); } &:focus { border-color: transparent; } + + transition: border-color $transition-color-length $transition-delay + $transition-chevron-rotate-length * 2 ease-in-out; +} + +.btn-sidebar-collapse::after, +.btn-sidebar-collapse::before { + content: ''; + left: 0; + top: 0; + width: 100%; + height: 100%; + z-index: -2; + position: absolute; + + transition: background-color $transition-color-length ease-in-out; +} + +.btn-sidebar-collapse::after { + background-color: var(--link-item-bg); +} + +.btn-sidebar-collapse::before { + opacity: 0; + z-index: -1; + background-color: var(--module-bg); + + transition: opacity $transition-color-length $transition-delay + $transition-chevron-rotate-length * 2 ease-in-out; +} + +.btn-sidebar-collapse:hover::after { + background-color: var(--sidebar-card-selected-bg); +} + +.btn-sidebar-collapse:hover::before { + background-color: var(--link-item-bg); +} + +.btn-sidebar-collapse:active::after { + background-color: var(--link-item-bg); +} + +.btn-sidebar-collapse:active::before { + background-color: var(--sidebar-card-selected-bg); +} + +.btn-sidebar-collapse.is-collapsed { + border-color: var(--bs-secondary); +} + +.is-collapsed.btn-sidebar-collapse::before { + opacity: 1; +} + +.btn-sidebar-collapse-chevron-start { + margin-right: -0.7rem; +} + +.btn-sidebar-collapse-chevron { + transform: rotateZ(-180deg); + display: inline-block; + overflow: hidden; + margin-left: 0.3rem; + + transition: transform $transition-chevron-rotate-length $transition-delay ease-in-out; +} + +.is-collapsed .btn-sidebar-collapse-chevron { + transform: rotateZ(0deg); + transition: transform $transition-chevron-rotate-length $transition-delay ease-in-out; } .three-dots { diff --git a/src/main/webapp/app/overview/course-overview.component.ts b/src/main/webapp/app/overview/course-overview.component.ts index 7cf4c805712e..86d8e31f26d3 100644 --- a/src/main/webapp/app/overview/course-overview.component.ts +++ b/src/main/webapp/app/overview/course-overview.component.ts @@ -18,6 +18,7 @@ import { IconDefinition, faChalkboardUser, faChartBar, + faChevronLeft, faChevronRight, faCircleNotch, faClipboard, @@ -31,6 +32,7 @@ import { faListCheck, faNetworkWired, faPersonChalkboard, + faQuestion, faSync, faTable, faTimes, @@ -64,6 +66,7 @@ import { ExamParticipationService } from 'app/exam/participate/exam-participatio import { CourseConversationsComponent } from 'app/overview/course-conversations/course-conversations.component'; import { sortCourses } from 'app/shared/util/course.util'; import { CourseUnenrollmentModalComponent } from './course-unenrollment-modal.component'; +import { LtiService } from 'app/shared/service/lti.service'; interface CourseActionItem { title: string; @@ -116,11 +119,14 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit isNotManagementView: boolean; canUnenroll: boolean; isNavbarCollapsed = false; + isSidebarCollapsed = false; profileSubscription?: Subscription; showRefreshButton: boolean = false; isExamStarted = false; private examStartedSubscription: Subscription; readonly MIN_DISPLAYED_COURSES: number = 6; + isLti: boolean = false; + private ltiSubscription: Subscription; // Properties to track hidden items for dropdown menu dropdownOpen: boolean = false; @@ -169,8 +175,10 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit faSync = faSync; faCircleNotch = faCircleNotch; faChevronRight = faChevronRight; + faChevronLeft = faChevronLeft; facSidebar = facSidebar; faEllipsis = faEllipsis; + faQuestion = faQuestion; FeatureToggle = FeatureToggle; CachingStrategy = CachingStrategy; @@ -194,6 +202,7 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit private profileService: ProfileService, private modalService: NgbModal, private examParticipationService: ExamParticipationService, + private ltiService: LtiService, ) {} async ngOnInit() { @@ -228,13 +237,19 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit this.courseActionItems = this.getCourseActionItems(); this.updateVisibleNavbarItems(window.innerHeight); await this.updateRecentlyAccessedCourses(); + this.isSidebarCollapsed = this.activatedComponentReference?.isCollapsed ?? false; + this.ltiSubscription = this.ltiService.isLti$.subscribe((isLti) => { + this.isLti = isLti; + }); } /** Listen window resize event by height */ @HostListener('window: resize', ['$event']) onResize() { - this.updateVisibleNavbarItems(window.innerHeight); - if (!this.anyItemHidden) this.itemsDrop.close(); + if (this.itemsDrop) { + this.updateVisibleNavbarItems(window.innerHeight); + if (!this.anyItemHidden) this.itemsDrop.close(); + } } /** Update sidebar item's hidden property based on the window height to display three-dots */ @@ -329,6 +344,12 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit sidebarItems.push(learningPathItem); } } + + if (this.course?.faqEnabled) { + const faqItem: SidebarItem = this.getFaqItem(); + sidebarItems.push(faqItem); + } + return sidebarItems; } @@ -437,6 +458,19 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit return dashboardItem; } + getFaqItem() { + const faqItem: SidebarItem = { + routerLink: 'faq', + icon: faQuestion, + title: 'FAQs', + translation: 'artemisApp.courseOverview.menu.faq', + hasInOrionProperty: false, + showInOrionWindow: false, + hidden: false, + }; + return faqItem; + } + getDefaultItems() { const items = []; if (this.course?.studentCourseAnalyticsDashboardEnabled) { @@ -560,6 +594,8 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit // Since we change the pageTitle + might be pulling data upwards during a render cycle, we need to re-run change detection this.changeDetectorRef.detectChanges(); + + this.isSidebarCollapsed = this.activatedComponentReference?.isCollapsed ?? false; } toggleSidebar() { @@ -568,6 +604,7 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit } const childRouteComponent = this.activatedComponentReference; childRouteComponent.toggleSidebar(); + this.isSidebarCollapsed = childRouteComponent.isCollapsed; } @HostListener('window:keydown.Control.Shift.b', ['$event']) @@ -712,6 +749,7 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit this.dashboardSubscription?.unsubscribe(); this.ngUnsubscribe.next(); this.ngUnsubscribe.complete(); + this.ltiSubscription?.unsubscribe(); } subscribeForQuizChanges() { diff --git a/src/main/webapp/app/overview/course-registration/course-registration-prerequisites-modal/course-prerequisites-modal.component.html b/src/main/webapp/app/overview/course-registration/course-registration-prerequisites-modal/course-prerequisites-modal.component.html index 3e4ef9383be6..c023ccafe635 100644 --- a/src/main/webapp/app/overview/course-registration/course-registration-prerequisites-modal/course-prerequisites-modal.component.html +++ b/src/main/webapp/app/overview/course-registration/course-registration-prerequisites-modal/course-prerequisites-modal.component.html @@ -13,7 +13,7 @@ } @else {
@for (prerequisite of prerequisites; track prerequisite.id; let i = $index) { - + }
} diff --git a/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups.component.html b/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups.component.html index c2acc5bebdc0..c6f3864b47b7 100644 --- a/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups.component.html +++ b/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups.component.html @@ -1,4 +1,4 @@ -
+
@if (course) {
diff --git a/src/main/webapp/app/overview/courses-routing.module.ts b/src/main/webapp/app/overview/courses-routing.module.ts index 8b011de2206c..4cb31090febf 100644 --- a/src/main/webapp/app/overview/courses-routing.module.ts +++ b/src/main/webapp/app/overview/courses-routing.module.ts @@ -255,6 +255,16 @@ const routes: Routes = [ pageTitle: 'overview.plagiarismCases', }, }, + { + path: 'faq', + loadComponent: () => import('../overview/course-faq/course-faq.component').then((m) => m.CourseFaqComponent), + data: { + authorities: [Authority.USER], + pageTitle: 'overview.faq', + hasSidebar: false, + showRefreshButton: true, + }, + }, { path: '', redirectTo: 'dashboard', // dashboard will redirect to exercises if not enabled diff --git a/src/main/webapp/app/overview/courses.component.html b/src/main/webapp/app/overview/courses.component.html index 92381c38c62b..5836c04911c3 100644 --- a/src/main/webapp/app/overview/courses.component.html +++ b/src/main/webapp/app/overview/courses.component.html @@ -1,8 +1,8 @@ @if (nextRelevantExam && nextRelevantCourseForExam) { -
+

-
-
+
+

{{ nextRelevantExam.title }}

@@ -19,43 +19,66 @@

{{ nextRelevantExam.title }}

} -
-
-

-
- - @if (regularCourses.length) { -
- +
+
+

{{ 'artemisApp.studentDashboard.title' | artemisTranslate }} ({{ regularCourses.length + recentlyAccessedCourses.length }})

+
+ + + @if (regularCourses.length) { + + }
- } -
-@if (recentlyAccessedCourses.length > 0) { -
-

-
- @for (course of recentlyAccessedCourses; track course) { - - } -
- @if (regularCourses.length > 0) { + @if (recentlyAccessedCourses.length) {
-

+

+ + @if (regularCourses.length) { +
+

+
+ } } -} -@if (coursesLoaded && !regularCourses.length && !recentlyAccessedCourses.length) { -
-

-
- + @if (coursesLoaded && !regularCourses.length && !recentlyAccessedCourses.length) { +
+

+
+ +
-
-} @else { -
- @for (course of regularCourses; track course) { - + } @else { + + } +
+ + + @if ((courses | searchFilter: ['title'] : searchCourseText).length > 0) { +
+
+ @for (course of courses | searchFilter: ['title'] : searchCourseText; track course) { +
+ +
+ } +
+
+ } @else { + @if (coursesLoaded) { +
+ +
} -
-} + } +
diff --git a/src/main/webapp/app/overview/courses.component.scss b/src/main/webapp/app/overview/courses.component.scss index 93bd4a898edf..90047d3652c4 100644 --- a/src/main/webapp/app/overview/courses.component.scss +++ b/src/main/webapp/app/overview/courses.component.scss @@ -14,25 +14,24 @@ opacity: 1; } -.col-12 { - width: 100% !important; -} - -@media screen and (min-width: 992px) { - .col-lg-6 { - width: 50% !important; +.course-grid { + display: grid; + // cards can shrink to 350px + grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); + grid-gap: 1rem; + justify-items: center; + // for 4 or less cards, we let each card shrink to 325px to display more cards in smaller screens, 1751px is the breakpoint for 4 cards + @media screen and (max-width: 1751px) { + grid-template-columns: repeat(auto-fill, minmax(325px, 1fr)); } } -@media screen and (min-width: 1200px) { - .col-xl-4 { - width: 33.33% !important; - } +.course-card-wrapper { + width: 100%; + max-width: 400px; } -@media screen and (min-width: 1900px) { - .col-xl-4 { - flex: 0 0 25; - width: 25% !important; - } +.container-fluid { + // ensure that horizontal spacing in container is consistent + --bs-gutter-x: 2rem; } diff --git a/src/main/webapp/app/overview/courses.component.ts b/src/main/webapp/app/overview/courses.component.ts index 0d5218bb21b0..2ad16cae7fcd 100644 --- a/src/main/webapp/app/overview/courses.component.ts +++ b/src/main/webapp/app/overview/courses.component.ts @@ -10,7 +10,7 @@ import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; import dayjs from 'dayjs/esm'; import { Exam } from 'app/entities/exam/exam.model'; import { Router } from '@angular/router'; -import { faPenAlt } from '@fortawesome/free-solid-svg-icons'; +import { faArrowDownAZ, faArrowUpAZ, faDoorOpen, faPenAlt } from '@fortawesome/free-solid-svg-icons'; import { CourseAccessStorageService } from 'app/course/course-access-storage.service'; import { CourseForDashboardDTO } from 'app/course/manage/course-for-dashboard-dto'; import { sortCourses } from 'app/shared/util/course.util'; @@ -21,6 +21,11 @@ import { sortCourses } from 'app/shared/util/course.util'; styleUrls: ['./courses.component.scss'], }) export class CoursesComponent implements OnInit, OnDestroy { + protected readonly faPenAlt = faPenAlt; + protected readonly faArrowDownAZ = faArrowDownAZ; + protected readonly faArrowUpAZ = faArrowUpAZ; + protected readonly faDoorOpen = faDoorOpen; + courses: Course[]; public nextRelevantCourse?: Course; nextRelevantCourseForExam?: Course; @@ -31,11 +36,10 @@ export class CoursesComponent implements OnInit, OnDestroy { courseForGuidedTour?: Course; quizExercisesChannels: string[] = []; - - // Icons - faPenAlt = faPenAlt; + searchCourseText = ''; coursesLoaded = false; + isSortAscending = true; constructor( private courseService: CourseManagementService, @@ -49,6 +53,7 @@ export class CoursesComponent implements OnInit, OnDestroy { async ngOnInit() { this.loadAndFilterCourses(); (await this.teamService.teamAssignmentUpdates).subscribe(); + this.courseService.enableCourseOverviewBackground(); } /** @@ -58,6 +63,7 @@ export class CoursesComponent implements OnInit, OnDestroy { if (this.quizExercisesChannels) { this.quizExercisesChannels.forEach((channel) => this.jhiWebsocketService.unsubscribe(channel)); } + this.courseService.disableCourseOverviewBackground(); } loadAndFilterCourses() { @@ -123,4 +129,19 @@ export class CoursesComponent implements OnInit, OnDestroy { openExam(): void { this.router.navigate(['courses', this.nextRelevantCourseForExam?.id, 'exams', this.nextRelevantExam!.id]); } + + setSearchValue(searchValue: string): void { + this.searchCourseText = searchValue; + } + + /** + * Sorts the courses in alphabetical order + */ + onSort(): void { + if (this.courses) { + this.isSortAscending = !this.isSortAscending; + this.regularCourses = [...sortCourses(this.regularCourses, this.isSortAscending)]; + this.recentlyAccessedCourses = [...sortCourses(this.recentlyAccessedCourses, this.isSortAscending)]; + } + } } diff --git a/src/main/webapp/app/overview/courses.module.ts b/src/main/webapp/app/overview/courses.module.ts index 01e27a171163..5dbae0d30fde 100644 --- a/src/main/webapp/app/overview/courses.module.ts +++ b/src/main/webapp/app/overview/courses.module.ts @@ -19,6 +19,7 @@ import { ArtemisCourseExerciseRowModule } from 'app/overview/course-exercises/co import { NgxChartsModule, PieChartModule } from '@swimlane/ngx-charts'; import { CourseUnenrollmentModalComponent } from 'app/overview/course-unenrollment-modal.component'; import { ArtemisSidebarModule } from 'app/shared/sidebar/sidebar.module'; +import { SearchFilterComponent } from 'app/shared/search-filter/search-filter.component'; @NgModule({ imports: [ @@ -36,6 +37,7 @@ import { ArtemisSidebarModule } from 'app/shared/sidebar/sidebar.module'; NgxChartsModule, PieChartModule, ArtemisSidebarModule, + SearchFilterComponent, ], declarations: [ CoursesComponent, diff --git a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html index a36bbde86268..29582939ece9 100644 --- a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html +++ b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html @@ -241,7 +241,7 @@

} @if (exercise.type === PROGRAMMING && !exercise.exerciseGroup && irisSettings?.irisChatSettings?.enabled) { - + } { if (participation.results) { - participation.results = participation.results.filter((result: Result) => result.completionDate && result.successful !== undefined); + participation.results = participation.results.filter((result: Result) => result.completionDate); } }); } @@ -254,7 +255,7 @@ export class CourseExerciseDetailsComponent extends AbstractScienceComponent imp this.sortedHistoryResults = this.studentParticipations .flatMap((participation) => participation.results ?? []) .sort(this.resultSortFunction) - .filter((result) => !(result.assessmentType === AssessmentType.AUTOMATIC_ATHENA && result.successful == undefined)); + .filter((result) => !(result.assessmentType === AssessmentType.AUTOMATIC_ATHENA && dayjs().isBefore(result.completionDate))); } } diff --git a/src/main/webapp/app/overview/exercise-details/exercise-buttons.module.ts b/src/main/webapp/app/overview/exercise-details/exercise-buttons.module.ts index 1b36ab7e6f5d..7b6aab3d5f7c 100644 --- a/src/main/webapp/app/overview/exercise-details/exercise-buttons.module.ts +++ b/src/main/webapp/app/overview/exercise-details/exercise-buttons.module.ts @@ -6,9 +6,10 @@ import { OrionExerciseDetailsStudentActionsComponent } from 'app/orion/participa import { ExerciseDetailsStudentActionsComponent } from 'app/overview/exercise-details/exercise-details-student-actions.component'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { ArtemisSharedPipesModule } from 'app/shared/pipes/shared-pipes.module'; +import { RequestFeedbackButtonComponent } from 'app/overview/exercise-details/request-feedback-button/request-feedback-button.component'; @NgModule({ - imports: [ArtemisSharedModule, ArtemisSharedComponentModule, ArtemisSharedPipesModule, OrionModule, FeatureToggleModule], + imports: [ArtemisSharedModule, ArtemisSharedComponentModule, ArtemisSharedPipesModule, OrionModule, FeatureToggleModule, RequestFeedbackButtonComponent], declarations: [ExerciseDetailsStudentActionsComponent, OrionExerciseDetailsStudentActionsComponent], exports: [ExerciseDetailsStudentActionsComponent, OrionExerciseDetailsStudentActionsComponent], }) diff --git a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html index 53a99d1f440a..6e2df76cbef9 100644 --- a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html +++ b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html @@ -135,30 +135,8 @@ } - @if (exercise.allowFeedbackRequests) { - @if (athenaEnabled) { - - - Send automatic feedback request - - } @else { - - - Send manual feedback request - - } + @if (exercise.allowFeedbackRequests && gradedParticipation && exercise.type === ExerciseType.PROGRAMMING) { + } } diff --git a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts index fcdf131a87c4..2991bac3355f 100644 --- a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts +++ b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts @@ -110,6 +110,7 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges this.profileService.getProfileInfo().subscribe((profileInfo) => { this.localVCEnabled = profileInfo.activeProfiles?.includes(PROFILE_LOCALVC); this.athenaEnabled = profileInfo.activeProfiles?.includes(PROFILE_ATHENA); + // The online IDE is only available with correct SpringProfile and if it's enabled for this exercise if (profileInfo.activeProfiles?.includes(PROFILE_THEIA) && this.programmingExercise) { this.theiaEnabled = true; @@ -257,6 +258,7 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges }); } + // TODO remove this method once support of the button component is implemented for text and modeling exercises requestFeedback() { if (!this.assureConditionsSatisfied()) return; if (this.exercise.type === ExerciseType.PROGRAMMING) { @@ -341,6 +343,7 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges * 3. There is no already pending feedback request. * @returns {boolean} `true` if all conditions are satisfied, otherwise `false`. */ + // TODO remove this method once support of the button component is implemented for text and modeling exercises assureConditionsSatisfied(): boolean { this.updateParticipations(); if (this.exercise.type === ExerciseType.PROGRAMMING) { @@ -378,7 +381,7 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges } } - if (this.hasAthenaResultForlatestSubmission()) { + if (this.hasAthenaResultForLatestSubmission()) { const submitFirstWarning = this.translateService.instant('artemisApp.exercise.submissionAlreadyHasAthenaResult'); this.alertService.warning(submitFirstWarning); return false; @@ -386,29 +389,14 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges return true; } - hasAthenaResultForlatestSubmission(): boolean { + hasAthenaResultForLatestSubmission(): boolean { if (this.gradedParticipation?.submissions && this.gradedParticipation?.results) { - const sortedSubmissions = this.gradedParticipation.submissions.slice().sort((a, b) => { - const dateA = this.getDateValue(a.submissionDate) ?? -Infinity; - const dateB = this.getDateValue(b.submissionDate) ?? -Infinity; - return dateB - dateA; - }); - - return this.gradedParticipation.results.some((result) => result.submission?.id === sortedSubmissions[0]?.id); + // submissions.results is always undefined so this is necessary + return ( + this.gradedParticipation.submissions.last()?.id === + this.gradedParticipation?.results.filter((result) => result.assessmentType == AssessmentType.AUTOMATIC_ATHENA).first()?.submission?.id + ); } return false; } - - private getDateValue = (date: any): number => { - if (dayjs.isDayjs(date)) { - return date.valueOf(); - } - if (date instanceof Date) { - return date.valueOf(); - } - if (typeof date === 'string') { - return new Date(date).valueOf(); - } - return -Infinity; // fallback for null, undefined, or invalid dates - }; } diff --git a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.html b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.html new file mode 100644 index 000000000000..6d6addcc2b84 --- /dev/null +++ b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.html @@ -0,0 +1,39 @@ +@if (!isExamExercise) { + @if (athenaEnabled) { + @if (exercise().type === ExerciseType.TEXT) { + + } @else { + + + + + } + } @else { + + + + + } +} diff --git a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts new file mode 100644 index 000000000000..b9aecaec56d5 --- /dev/null +++ b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts @@ -0,0 +1,117 @@ +import { Component, OnInit, inject, input, output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { faPenSquare } from '@fortawesome/free-solid-svg-icons'; +import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; +import { PROFILE_ATHENA } from 'app/app.constants'; +import { StudentParticipation } from 'app/entities/participation/student-participation.model'; +import { Exercise, ExerciseType } from 'app/entities/exercise.model'; +import { AlertService } from 'app/core/util/alert.service'; +import { CourseExerciseService } from 'app/exercises/shared/course-exercises/course-exercise.service'; +import { AssessmentType } from 'app/entities/assessment-type.model'; +import { TranslateService } from '@ngx-translate/core'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; +import { isExamExercise } from 'app/shared/util/utils'; +import { ExerciseDetailsType, ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; +import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; +import { ParticipationService } from 'app/exercises/shared/participation/participation.service'; + +@Component({ + selector: 'jhi-request-feedback-button', + standalone: true, + imports: [CommonModule, ArtemisSharedCommonModule, NgbTooltipModule, FontAwesomeModule], + templateUrl: './request-feedback-button.component.html', +}) +export class RequestFeedbackButtonComponent implements OnInit { + faPenSquare = faPenSquare; + athenaEnabled = false; + isExamExercise: boolean; + participation?: StudentParticipation; + + isGeneratingFeedback = input(); + smallButtons = input(false); + exercise = input.required(); + generatingFeedback = output(); + + private feedbackSent = false; + private profileService = inject(ProfileService); + private alertService = inject(AlertService); + private courseExerciseService = inject(CourseExerciseService); + private translateService = inject(TranslateService); + private exerciseService = inject(ExerciseService); + private participationService = inject(ParticipationService); + + protected readonly ExerciseType = ExerciseType; + + ngOnInit() { + this.profileService.getProfileInfo().subscribe((profileInfo) => { + this.athenaEnabled = profileInfo.activeProfiles?.includes(PROFILE_ATHENA); + }); + this.isExamExercise = isExamExercise(this.exercise()); + if (this.isExamExercise || !this.exercise().id) { + return; + } + this.updateParticipation(); + } + + private updateParticipation() { + if (this.exercise().id) { + this.exerciseService.getExerciseDetails(this.exercise().id!).subscribe({ + next: (exerciseResponse: HttpResponse) => { + this.participation = this.participationService.getSpecificStudentParticipation(exerciseResponse.body!.exercise.studentParticipations ?? [], false); + }, + error: (error: HttpErrorResponse) => { + this.alertService.error(`artemisApp.${error.error.entityName}.errors.${error.error.errorKey}`); + }, + }); + } + } + + requestFeedback() { + if (!this.assureConditionsSatisfied()) { + return; + } + + this.courseExerciseService.requestFeedback(this.exercise().id!).subscribe({ + next: (participation: StudentParticipation) => { + if (participation) { + this.generatingFeedback.emit(); + this.feedbackSent = true; + this.alertService.success('artemisApp.exercise.feedbackRequestSent'); + } + }, + error: (error: HttpErrorResponse) => { + this.alertService.error(`artemisApp.exercise.${error.error.errorKey}`); + }, + }); + } + + /** + * Checks if the conditions for requesting automatic non-graded feedback are satisfied. + * The student can request automatic non-graded feedback under the following conditions: + * 1. They have a graded submission. + * 2. The deadline for the exercise has not been exceeded. + * 3. There is no already pending feedback request. + * @returns {boolean} `true` if all conditions are satisfied, otherwise `false`. + */ + assureConditionsSatisfied(): boolean { + if (this.exercise().type === ExerciseType.PROGRAMMING || !this.hasAthenaResultForLatestSubmission()) { + return true; + } + const submitFirstWarning = this.translateService.instant('artemisApp.exercise.submissionAlreadyHasAthenaResult'); + this.alertService.warning(submitFirstWarning); + return false; + } + + hasAthenaResultForLatestSubmission(): boolean { + if (this.participation?.submissions && this.participation?.results) { + // submissions.results is always undefined so this is neccessary + return ( + this.participation.submissions?.last()?.id === + this.participation.results?.filter((result) => result.assessmentType == AssessmentType.AUTOMATIC_ATHENA).first()?.submission?.id + ); + } + return false; + } +} diff --git a/src/main/webapp/app/shared/category-selector/category-selector.component.ts b/src/main/webapp/app/shared/category-selector/category-selector.component.ts index d899a7b034b3..4214f340ffca 100644 --- a/src/main/webapp/app/shared/category-selector/category-selector.component.ts +++ b/src/main/webapp/app/shared/category-selector/category-selector.component.ts @@ -7,6 +7,7 @@ import { FormControl } from '@angular/forms'; import { MatChipInputEvent } from '@angular/material/chips'; import { Observable, map, startWith } from 'rxjs'; import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import { FaqCategory } from 'app/entities/faq-category.model'; const DEFAULT_COLORS = ['#6ae8ac', '#9dca53', '#94a11c', '#691b0b', '#ad5658', '#1b97ca', '#0d3cc2', '#0ab84f']; @@ -22,12 +23,12 @@ export class CategorySelectorComponent implements OnChanges { /** * the selected categories, which can be manipulated by the user in the UI */ - @Input() categories: ExerciseCategory[]; + @Input() categories: ExerciseCategory[] | FaqCategory[]; /** * the existing categories used for auto-completion, might include duplicates */ - @Input() existingCategories: ExerciseCategory[]; + @Input() existingCategories: ExerciseCategory[] | FaqCategory[]; @Output() selectedCategories = new EventEmitter(); diff --git a/src/main/webapp/app/shared/components/documentation-button/documentation-button.component.ts b/src/main/webapp/app/shared/components/documentation-button/documentation-button.component.ts index f598865bb6a6..7724361d798e 100644 --- a/src/main/webapp/app/shared/components/documentation-button/documentation-button.component.ts +++ b/src/main/webapp/app/shared/components/documentation-button/documentation-button.component.ts @@ -12,7 +12,7 @@ const DocumentationLinks = { Quiz: 'exercises/quiz/', Model: 'exercises/modeling/', Programming: 'exercises/programming/', - SshSetup: 'exercises/programming.html#repository-access', + SshSetup: 'icl/ssh-intro', Text: 'exercises/textual/', FileUpload: 'exercises/file-upload/', Notifications: 'notifications/', diff --git a/src/main/webapp/app/shared/components/documentation-link/documentation-link.component.html b/src/main/webapp/app/shared/components/documentation-link/documentation-link.component.html new file mode 100644 index 000000000000..b3e990001b2f --- /dev/null +++ b/src/main/webapp/app/shared/components/documentation-link/documentation-link.component.html @@ -0,0 +1,5 @@ +@if (displayString() && documentationType()) { + + + +} diff --git a/src/main/webapp/app/shared/components/documentation-link/documentation-link.component.ts b/src/main/webapp/app/shared/components/documentation-link/documentation-link.component.ts new file mode 100644 index 000000000000..86f5d19aafb6 --- /dev/null +++ b/src/main/webapp/app/shared/components/documentation-link/documentation-link.component.ts @@ -0,0 +1,25 @@ +import { Component, input } from '@angular/core'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; + +// The routes here are used to build the link to the documentation. +// Therefore, it's important that they exactly match the url to the subpage of the documentation. +// Additionally, the case names must match the keys in documentationLinks.json for the tooltip. +const DocumentationLinks: { [key: string]: string } = { + SshSetup: 'icl/ssh-intro', +}; + +export type DocumentationType = keyof typeof DocumentationLinks; + +@Component({ + selector: 'jhi-documentation-link', + standalone: true, + templateUrl: './documentation-link.component.html', + imports: [TranslateDirective], +}) +export class DocumentationLinkComponent { + readonly BASE_URL = 'https://docs.artemis.cit.tum.de/user/'; + readonly DocumentationLinks = DocumentationLinks; + + documentationType = input(); + displayString = input(); +} diff --git a/src/main/webapp/app/shared/constants/file-extensions.constants.ts b/src/main/webapp/app/shared/constants/file-extensions.constants.ts index 363f41e6b19b..3d1797cb308d 100644 --- a/src/main/webapp/app/shared/constants/file-extensions.constants.ts +++ b/src/main/webapp/app/shared/constants/file-extensions.constants.ts @@ -1,16 +1,16 @@ /** * The list of file extensions that are allowed to be uploaded in a Markdown editor. * Extensions must be lower-case without leading dots. - * NOTE: Has to be kept in sync with the server-side definitions in FileResource.java + * NOTE: Has to be kept in sync with the server-side definitions in FileService.java */ -export const MARKDOWN_FILE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'pdf']; +export const UPLOAD_MARKDOWN_FILE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'pdf']; /** * The global list of file extensions that are allowed to be uploaded. * Extensions must be lower-case without leading dots. - * NOTE: Has to be kept in sync with the server-side definitions in FileResource.java + * NOTE: Has to be kept in sync with the server-side definitions in FileService.java */ -export const FILE_EXTENSIONS = [ +export const UPLOAD_FILE_EXTENSIONS = [ 'png', 'jpg', 'jpeg', @@ -44,3 +44,302 @@ export const FILE_EXTENSIONS = [ 'odi', 'odf', ]; + +/** + * The list of file extensions that are readable in a file editor. + * Extensions are case-sensitive. + * Extensions are the part of the filename after the last dot. + * Files without dots are represented with their full filename. + */ +export const TEXT_FILE_EXTENSIONS = [ + 'Makefile', + 'R', + 'Rakefile', + 'ada', + 'adb', + 'ads', + 'applescript', + 'as', + 'ascx', + 'asm', + 'asmx', + 'asp', + 'aspx', + 'atom', + 'bas', + 'bash', + 'bashrc', + 'bat', + 'bbcolors', + 'bdsgroup', + 'bdsproj', + 'bib', + 'bowerrc', + 'c', + 'cbl', + 'cc', + 'cfc', + 'cfg', + 'cfm', + 'cfml', + 'cgi', + 'clj', + 'cls', + 'cmake', + 'cmd', + 'cnf', + 'cob', + 'coffee', + 'coffeekup', + 'conf', + 'cpp', + 'cpt', + 'cpy', + 'crt', + 'cs', + 'csh', + 'cson', + 'csr', + 'css', + 'csslintrc', + 'csv', + 'ctl', + 'curlrc', + 'cxx', + 'dart', + 'dfm', + 'diff', + 'dof', + 'dpk', + 'dproj', + 'dtd', + 'eco', + 'editorconfig', + 'ejs', + 'el', + 'emacs', + 'eml', + 'ent', + 'erb', + 'erl', + 'eslintignore', + 'eslintrc', + 'ex', + 'exp', + 'exs', + 'f', + 'f03', + 'f77', + 'f90', + 'f95', + 'fish', + 'for', + 'fpp', + 'frm', + 'ftn', + 'gemrc', + 'gitattributes', + 'gitconfig', + 'gitignore', + 'gitkeep', + 'gitmodules', + 'go', + 'gpp', + 'gradle', + 'groovy', + 'groupproj', + 'grunit', + 'gtmpl', + 'gvimrc', + 'h', + 'haml', + 'hbs', + 'hgignore', + 'hh', + 'hpp', + 'hrl', + 'hs', + 'hta', + 'htaccess', + 'htc', + 'htm', + 'html', + 'htpasswd', + 'hxx', + 'iced', + 'inc', + 'ini', + 'ino', + 'int', + 'irbrc', + 'itcl', + 'itermcolors', + 'itk', + 'jade', + 'java', + 'jhtm', + 'jhtml', + 'js', + 'jscsrc', + 'jshintignore', + 'jshintrc', + 'json', + 'json5', + 'jsonld', + 'jsp', + 'jspx', + 'jsx', + 'ksh', + 'kt', + 'less', + 'lhs', + 'lisp', + 'log', + 'ls', + 'lsp', + 'lua', + 'm', + 'mak', + 'map', + 'markdown', + 'master', + 'md', + 'mdown', + 'mdwn', + 'mdx', + 'metadata', + 'mht', + 'mhtml', + 'mjs', + 'mk', + 'mkd', + 'mkdn', + 'mkdown', + 'ml', + 'mli', + 'mm', + 'mxml', + 'nfm', + 'nfo', + 'noon', + 'npmignore', + 'npmrc', + 'nvmrc', + 'ops', + 'pas', + 'pasm', + 'patch', + 'pbxproj', + 'pch', + 'pem', + 'pg', + 'php', + 'php3', + 'php4', + 'php5', + 'phpt', + 'phtml', + 'pir', + 'pl', + 'pm', + 'pmc', + 'pod', + 'pot', + 'properties', + 'props', + 'pt', + 'pug', + 'py', + 'r', + 'rake', + 'rb', + 'rdoc', + 'rdoc_options', + 'resx', + 'rhtml', + 'rjs', + 'rlib', + 'ron', + 'rs', + 'rss', + 'rst', + 'rtf', + 'rvmrc', + 'rxml', + 's', + 'sass', + 'scala', + 'scm', + 'scss', + 'seestyle', + 'sh', + 'shtml', + 'sls', + 'spec', + 'sql', + 'sqlite', + 'ss', + 'sss', + 'st', + 'strings', + 'sty', + 'styl', + 'stylus', + 'sub', + 'sublime-build', + 'sublime-commands', + 'sublime-completions', + 'sublime-keymap', + 'sublime-macro', + 'sublime-menu', + 'sublime-project', + 'sublime-settings', + 'sublime-workspace', + 'sv', + 'svc', + 'svg', + 'swift', + 't', + 'tcl', + 'tcsh', + 'terminal', + 'tex', + 'text', + 'textile', + 'tg', + 'tmLanguage', + 'tmTheme', + 'tmpl', + 'tpl', + 'ts', + 'tsv', + 'tsx', + 'tt', + 'tt2', + 'ttml', + 'txt', + 'v', + 'vb', + 'vbs', + 'vh', + 'vhd', + 'vhdl', + 'vim', + 'viminfo', + 'vimrc', + 'vue', + 'webapp', + 'wxml', + 'x-php', + 'xht', + 'xhtml', + 'xml', + 'xs', + 'xsd', + 'xsl', + 'xslt', + 'yaml', + 'yml', + 'zsh', + 'zshrc', +]; diff --git a/src/main/webapp/app/shared/constants/input.constants.ts b/src/main/webapp/app/shared/constants/input.constants.ts index 8b97589a59d6..1b1045b7f56f 100644 --- a/src/main/webapp/app/shared/constants/input.constants.ts +++ b/src/main/webapp/app/shared/constants/input.constants.ts @@ -22,3 +22,5 @@ export const MAX_QUIZ_QUESTION_POINTS = 9999; export const MAX_QUIZ_QUESTION_LENGTH_THRESHOLD = 250; export const MAX_QUIZ_QUESTION_EXPLANATION_LENGTH_THRESHOLD = 500; export const MAX_QUIZ_QUESTION_HINT_LENGTH_THRESHOLD = 255; +export const ASSIGNMENT_REPO_NAME = 'assignment'; +export const TEST_REPO_NAME = 'tests'; diff --git a/src/main/webapp/app/shared/delete-dialog/delete-button.directive.ts b/src/main/webapp/app/shared/delete-dialog/delete-button.directive.ts index 4f1476ae508b..6fe508245dc8 100644 --- a/src/main/webapp/app/shared/delete-dialog/delete-button.directive.ts +++ b/src/main/webapp/app/shared/delete-dialog/delete-button.directive.ts @@ -1,7 +1,7 @@ import { DeleteDialogService } from 'app/shared/delete-dialog/delete-dialog.service'; import { Directive, ElementRef, EventEmitter, HostListener, Input, OnInit, Output, Renderer2 } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { ActionType, DeleteDialogData } from 'app/shared/delete-dialog/delete-dialog.model'; +import { ActionType, DeleteDialogData, EntitySummary } from 'app/shared/delete-dialog/delete-dialog.model'; import { Observable } from 'rxjs'; import { ButtonSize, ButtonType } from 'app/shared/components/button.component'; @@ -9,6 +9,8 @@ import { ButtonSize, ButtonType } from 'app/shared/components/button.component'; export class DeleteButtonDirective implements OnInit { @Input() entityTitle?: string; @Input() deleteQuestion: string; + @Input() entitySummaryTitle?: string; + @Input() fetchEntitySummary?: Observable; @Input() translateValues: { [key: string]: unknown } = {}; @Input() deleteConfirmationText: string; @Input() buttonSize: ButtonSize = ButtonSize.SMALL; @@ -73,6 +75,8 @@ export class DeleteButtonDirective implements OnInit { translateValues: this.translateValues, deleteConfirmationText: this.deleteConfirmationText, additionalChecks: this.additionalChecks, + entitySummaryTitle: this.entitySummaryTitle, + fetchEntitySummary: this.fetchEntitySummary, actionType: this.actionType, buttonType: this.buttonType, delete: this.delete, diff --git a/src/main/webapp/app/shared/delete-dialog/delete-dialog.component.html b/src/main/webapp/app/shared/delete-dialog/delete-dialog.component.html index eaadd2bb1acd..47e8a7fd5b6c 100644 --- a/src/main/webapp/app/shared/delete-dialog/delete-dialog.component.html +++ b/src/main/webapp/app/shared/delete-dialog/delete-dialog.component.html @@ -29,6 +29,22 @@