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/.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/build.gradle b/build.gradle index 1375d3298583..29da90bf3674 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 { @@ -252,6 +252,8 @@ dependencies { implementation "de.jplag:swift:${jplag_version}" implementation "de.jplag:java:${jplag_version}" implementation "de.jplag:python-3:${jplag_version}" + implementation "de.jplag:rust:${jplag_version}" + implementation "de.jplag:javascript:${jplag_version}" implementation "de.jplag:text:${jplag_version}" // those are transitive dependencies of JPlag Text --> Stanford NLP @@ -270,7 +272,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 +344,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 +407,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 +418,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 +466,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,7 +530,7 @@ 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" diff --git a/docs/user/exercises/programming-exercise-features.inc b/docs/user/exercises/programming-exercise-features.inc index 6d0b257dd96e..7bccf1596315 100644 --- a/docs/user/exercises/programming-exercise-features.inc +++ b/docs/user/exercises/programming-exercise-features.inc @@ -67,9 +67,9 @@ 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 | no | no | n/a | no | no | L: yes, J: no | + | JavaScript | 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/gradle.properties b/gradle.properties index fc54fad2849b..46526dddaf56 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,8 +18,8 @@ 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 @@ -36,11 +36,11 @@ byte_buddy_version=1.15.3 # make sure both versions are compatible junit_version=5.11.0 junit_platform_version=1.11.1 -mockito_version=5.13.0 +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..3dab49d4b7e0 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.37, + branches: 73.68, + functions: 81.93, + lines: 87.42, }, }, coverageReporters: ['clover', 'json', 'lcov', 'text-summary'], diff --git a/package-lock.json b/package-lock.json index bc1d3b7dff03..c72840ee901d 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.7", + "@angular/cdk": "18.2.7", + "@angular/common": "18.2.7", + "@angular/compiler": "18.2.7", + "@angular/core": "18.2.7", + "@angular/forms": "18.2.7", + "@angular/localize": "18.2.7", + "@angular/material": "18.2.7", + "@angular/platform-browser": "18.2.7", + "@angular/platform-browser-dynamic": "18.2.7", + "@angular/router": "18.2.7", + "@angular/service-worker": "18.2.7", "@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.33.1", "@siemens/ngx-datatable": "22.4.1", "@swimlane/ngx-charts": "20.5.0", "@swimlane/ngx-graph": "8.4.0", @@ -60,7 +60,7 @@ "ngx-webstorage": "18.0.0", "papaparse": "5.4.1", "pdfjs-dist": "4.6.82", - "posthog-js": "1.165.0", + "posthog-js": "1.166.1", "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.7", "@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.7", + "@angular/compiler-cli": "18.2.7", + "@angular/language-service": "18.2.7", + "@sentry/types": "8.33.1", "@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.4", "@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.0", + "@typescript-eslint/parser": "8.8.0", + "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.4", "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.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.7.tgz", + "integrity": "sha512-kpcgXnepEXcoxDTbqbGj7Hg1WJLWj1HLR3/FKmC5TbpBf1xiLxiqfkQNwz3BbE/W9JWMLdrXr3GI9O3O2gWPLg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.6", + "@angular-devkit/core": "18.2.7", "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.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.7.tgz", + "integrity": "sha512-u8PriYdgddK7k+OS/pOFPD1v4Iu5bztUJZXZVcGeXBZFFdnGFFzKmQw9mfcyGvTMJp2ABgBuuJT0YqYgNfAhzw==", "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.7", + "@angular-devkit/build-webpack": "0.1802.7", + "@angular-devkit/core": "18.2.7", + "@angular/build": "18.2.7", "@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.7", "@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.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.7.tgz", + "integrity": "sha512-VrtbrhZ+dht3f0GjtfRLRGRN4XHN/W+/bA9DqckdxVS6SydsrCWNHonvEPmOs4jJmGIGXIu6tUBMcWleTao2sg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1802.6", + "@angular-devkit/architect": "0.1802.7", "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.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.7.tgz", + "integrity": "sha512-1ZTi4A6tEC2bkJ/puCIdIPYhesnlCVOMSDJL/lZAd0hC6X22T4pwu0AEvue7mcP5NbXpQDiBaXOZ3MmCA8PwOA==", "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.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.7.tgz", + "integrity": "sha512-j7198lpkOXMG+Gyfln/5aDgBZV7m4pWMzHFhkO3+w3cbCNUN1TVZW0SyJcF+CYaxANzTbuumfvpsYc/fTeAGLw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.6", + "@angular-devkit/core": "18.2.7", "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.7", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.7.tgz", + "integrity": "sha512-5B7qD1K+kKOf9lgJT4VNMft3IK2BnRHjN1S6l38ywzQ/nxpmCG7f+qKAAU6CpCywhNUBeXW0hVXTMuMNPVOcQQ==", "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.7" } }, "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.7", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.7.tgz", + "integrity": "sha512-oq6JsVxLP9/w9F2IjKroJwPB9CdlMblu2Xhfq/qQZRSUuM8Ppt1svr2FBTo1HrLIbosqukkVcSSdmKYDneo+cg==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.6", + "@angular-devkit/architect": "0.1802.7", "@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.7", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.7.tgz", + "integrity": "sha512-Dfl37WBLeEUURQrDeuMcOgX2bkQJ+BGMOlr1qsFXzUWHH+qgYW2YwO1rbna/rjxyeFzc2Sy569dYRzNPqMewzg==", "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.7", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.7.tgz", + "integrity": "sha512-KoWgSvhRsU05A2m6B7jw1kdpyoS+Ce5GGLW6xcnX7VF2AckW54vYd/8ZkgpzQrKfvIpVblYd4KJGizKoaLZ5jA==", "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.7", + "@angular-devkit/core": "18.2.7", + "@angular-devkit/schematics": "18.2.7", "@inquirer/prompts": "5.3.8", "@listr2/prompt-adapter-inquirer": "2.0.15", - "@schematics/angular": "18.2.6", + "@schematics/angular": "18.2.7", "@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.7", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.7.tgz", + "integrity": "sha512-5vDBmBR2JcIxHVEDunKXNU+T+OvTGiHZTSo35GFOHJxKFgX5g6+0tJBZunK04oBZGbJQUmp3pg2kMvuKKjZnkQ==", "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.7", "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.7", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.7.tgz", + "integrity": "sha512-XemlYyRGnu/HrICtXwTPmGtyOrI8BhbGg/HMiJ7sVx40AeEIX0uyDgnu9Gc5OjmtDqZZ8Qftg1sQAxaCVjLb1w==", "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.7" }, "peerDependenciesMeta": { "@angular/core": { @@ -738,9 +738,9 @@ } }, "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.7", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.7.tgz", + "integrity": "sha512-U7cveObj+rrXH5EC8egAhATCeAAcOceEQDTVIOWmBa0qMR4hOMjtI2XUS2QRuI1Q+fQZ2hVEOW95WVLvEMsANA==", "license": "MIT", "dependencies": { "@babel/core": "7.25.2", @@ -761,14 +761,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "18.2.6", + "@angular/compiler": "18.2.7", "typescript": ">=5.4 <5.6" } }, "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.7", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.7.tgz", + "integrity": "sha512-hLOxgxLiyWm9iVHBsUsJfx1hDsXWZnfJBlr+N7cev53f0CDoPfbshqq6KV+JFqXFDguzR9dKHm1ewT1jK3e6Tw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -782,9 +782,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.7", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.7.tgz", + "integrity": "sha512-WO3c9/OA7ekBnDBgmvi5TlHshOt5S4NREIP+/VVyuRgg28BwUWyO/Nqh19nguE1UNNRt6OMLkT6NSV2ewhcXUg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -793,16 +793,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.7", + "@angular/core": "18.2.7", + "@angular/platform-browser": "18.2.7", "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.7", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-18.2.7.tgz", + "integrity": "sha512-gFsme3y5uC/dQGBBX05VnmT2KAEAZ6gsNk8m1b226LYvh8Oc+JQ4sXv7THGq1x5VnrTzRcCIELbkNHCiFdvL1Q==", "dev": true, "license": "MIT", "engines": { @@ -810,9 +810,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.7", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-18.2.7.tgz", + "integrity": "sha512-qYozomhO+1BlvtoMEEgKhaKz8thoztqNZEYPq9RmfkTB5uW7Q8h6rr1Sc2YAzJ6+ZA0McwabdJSX1TDxWyZx0Q==", "license": "MIT", "dependencies": { "@babel/core": "7.25.2", @@ -829,21 +829,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.7", + "@angular/compiler-cli": "18.2.7" } }, "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.7", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-18.2.7.tgz", + "integrity": "sha512-mgPj2TCIrsngmu3iNnoaPc6su7uPv+NPCv9HaiKhTx4QGae8EW+RvUxEZJvh4Qaym1fJTi3hjnVeWvQDLQt4CA==", "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.7", "@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 +852,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.7", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.7.tgz", + "integrity": "sha512-xgj2DH/isFrMZ73dJJm89NRnWBI3AHtugQrZbIapkKBdEt/C1o4SR2W2cV4mPb9o+ELnWurfrxFt9o/q2vnVLw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -863,9 +863,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.7", + "@angular/common": "18.2.7", + "@angular/core": "18.2.7" }, "peerDependenciesMeta": { "@angular/animations": { @@ -874,9 +874,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.7", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.7.tgz", + "integrity": "sha512-BDldzUKjnUjo0NW5gHjBY6CeJP1bWVfF1h/T3idyYG+F4Lxlb3aykRgLWXg4srNLY1KqE7XOYUmgc5cV613bgw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -885,16 +885,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.7", + "@angular/compiler": "18.2.7", + "@angular/core": "18.2.7", + "@angular/platform-browser": "18.2.7" } }, "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.7", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.7.tgz", + "integrity": "sha512-TXE8Aw63hDp3PEaNu4B1DMNvlS0uCzs36o/OSCCmewmLnzyJygkgi4jeEj20FsWPAQOUj5g5tnCYgxz1IRrCUg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -903,16 +903,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.7", + "@angular/core": "18.2.7", + "@angular/platform-browser": "18.2.7", "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.7", + "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-18.2.7.tgz", + "integrity": "sha512-1t8PUWmZi32i/SG/r12vz+cfn0l3xVEa0FY7GXaZK7hlfDL34js1HZXHkvGUuRZRw/4L1jl7AwPoxwGeWr2ldg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -924,17 +924,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.7", + "@angular/core": "18.2.7" } }, "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 +942,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.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.7.tgz", + "integrity": "sha512-9ickoLz+hcXCeh7jrcin+/SLWm+GkxE2kTvoYyp38p4WkdFXfQJxDFGWp/YHjiKLPx06z2A7W8XKuqbReXDzsw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1014,28 +1014,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 +1044,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 +1065,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 +1096,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 +1127,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 +1172,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 +1211,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 +1243,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 +1283,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 +1353,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.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.7.tgz", + "integrity": "sha512-aZn7ETtQsjjGG5HruveUK06cU3Hljuhd9Iojm4M8WWv3wLE6OkE5PWbDUkItmMgegmccaITudyuW5RPYrYlgWw==", "license": "MIT", "dependencies": { - "@babel/types": "^7.25.6" + "@babel/types": "^7.25.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -1329,14 +1368,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 +1385,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 +1401,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 +1417,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 +1435,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 +1546,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 +1604,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 +1729,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 +1762,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 +1815,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 +1831,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 +1847,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,14 +1864,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.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.25.7.tgz", + "integrity": "sha512-rvUUtoVlkDWtDWxGAiiQj0aNktTPn3eFynBcMC2IhsXweehwgdI9ODe+XjWw515kEmv22sSOTp/rxIRuTiB7zg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", "@babel/plugin-syntax-class-static-block": "^7.14.5" }, "engines": { @@ -1843,17 +1882,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 +1902,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 +1933,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 +1949,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 +1966,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 +1982,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,13 +1999,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.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.7.tgz", + "integrity": "sha512-UvcLuual4h7/GfylKm2IAA3aph9rwvAM2XBA0uPKU3lca+Maai4jBjjEVUS568ld6kJcgbouuumCBhMd/Yz17w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-plugin-utils": "^7.25.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3" }, "engines": { @@ -1964,14 +2016,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,13 +2033,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.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.7.tgz", + "integrity": "sha512-h3MDAP5l34NQkkNulsTNyjdaR+OiB0Im67VU//sFupouP8Q6m9Spy7l66DcaAQxtmCqGdanPByLsnwFttxKISQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-plugin-utils": "^7.25.7", "@babel/plugin-syntax-export-namespace-from": "^7.8.3" }, "engines": { @@ -1998,14 +2050,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 +2067,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,13 +2085,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.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.7.tgz", + "integrity": "sha512-Ot43PrL9TEAiCe8C/2erAjXMeVSnE/BLEx6eyrKLNFCCw5jvhTHKyHxdI1pA0kz5njZRYAnMO2KObGqOCRDYSA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-plugin-utils": "^7.25.7", "@babel/plugin-syntax-json-strings": "^7.8.3" }, "engines": { @@ -2050,13 +2102,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,13 +2118,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.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.7.tgz", + "integrity": "sha512-iImzbA55BjiovLyG2bggWS+V+OLkaBorNvc/yJoeeDQGztknRnDdYfp2d/UPmunZYEnZi6Lg8QcTmNMHOB0lGA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-plugin-utils": "^7.25.7", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" }, "engines": { @@ -2083,13 +2135,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 +2151,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 +2168,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 +2186,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 +2205,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 +2222,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 +2239,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,13 +2255,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.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.7.tgz", + "integrity": "sha512-FbuJ63/4LEL32mIxrxwYaqjJxpbzxPVQj5a+Ebrc8JICV6YX8nE53jY+K0RZT3um56GoNWgkS2BQ/uLGTjtwfw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-plugin-utils": "^7.25.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" }, "engines": { @@ -2220,13 +2272,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.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.7.tgz", + "integrity": "sha512-8CbutzSSh4hmD+jJHIA8vdTNk15kAzOnFLVVgBSMGr28rt85ouT01/rezMecks9pkU939wDInImwCKv4ahU4IA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-plugin-utils": "^7.25.7", "@babel/plugin-syntax-numeric-separator": "^7.10.4" }, "engines": { @@ -2237,16 +2289,16 @@ } }, "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.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.7.tgz", + "integrity": "sha512-1JdVKPhD7Y5PvgfFy0Mv2brdrolzpzSoUq2pr6xsR+m+3viGGeHEokFKsCgOkbeFOQxfB1Vt2F0cPJLRpFI4Zg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.24.7" + "@babel/plugin-transform-parameters": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2256,14 +2308,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,13 +2325,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.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.7.tgz", + "integrity": "sha512-m9obYBA39mDPN7lJzD5WkGGb0GO54PPLXsbcnj1Hyeu8mSRz7Gb4b1A6zxNX32ZuUySDK4G6it8SDFWD1nCnqg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-plugin-utils": "^7.25.7", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" }, "engines": { @@ -2290,14 +2342,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.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.7.tgz", + "integrity": "sha512-h39agClImgPWg4H8mYVAbD1qP9vClFbEjqoJmt87Zen8pjqK8FTPUwrOXAvqu5soytwxrLMd2fx2KSCp2CHcNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8", - "@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", "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, "engines": { @@ -2308,13 +2360,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 +2376,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,15 +2393,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.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.7.tgz", + "integrity": "sha512-LzA5ESzBy7tqj00Yjey9yWfs3FKy4EmJyKOSWld144OxkTji81WWnUT8nkLUn+imN/zHL8ZQlOu/MTUAhHaX3g==", "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/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, "engines": { @@ -2359,14 +2411,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 +2441,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 +2458,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 +2495,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 +2511,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 +2528,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 +2544,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 +2560,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 +2576,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 +2592,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 +2609,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 +2626,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 +2755,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 +2768,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 +2800,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.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.7.tgz", + "integrity": "sha512-vwIVdXG+j+FOpkwqHRcBgHLYNL7XMkufrlaFvL9o6Ai9sJn9+PdyIL5qa0XzTZw084c+u9LOls53eoZWP/W5WQ==", "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": { @@ -3464,9 +3534,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 +3633,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 +3672,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": { @@ -5265,9 +5359,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.7", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.7.tgz", + "integrity": "sha512-BmnFxss6zGobGyq9Mi7736golbK8RLgF+zYCQZ+4/OfMMA1jKVoELnyJqNyAx+DQn3m1qKVBjtGEL7pTNpPzOw==", "dev": true, "license": "MIT", "engines": { @@ -5597,23 +5691,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.3", + "resolved": "https://registry.npmjs.org/@nrwl/devkit/-/devkit-19.8.3.tgz", + "integrity": "sha512-67vZJRMCEA543A0uz8dPTZ5lX4wsAlgsr24KJafsUxBC2WCf9z4BqcLj0jVWfmRdKJmu2UwaxtD2UB1bekt3sg==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "19.6.5" + "@nx/devkit": "19.8.3" } }, "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.3", + "resolved": "https://registry.npmjs.org/@nrwl/tao/-/tao-19.8.3.tgz", + "integrity": "sha512-byjBtOXx+xGjMu1wKopJSJbrR3gKqTsCEgp1+YSZ45+iFKxFdXLJrGsyhVqBovCKVBM+5/KtGuEkZoUPlP8JWg==", "dev": true, "license": "MIT", "dependencies": { - "nx": "19.6.5", + "nx": "19.8.3", "tslib": "^2.3.0" }, "bin": { @@ -5621,13 +5715,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.3", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-19.8.3.tgz", + "integrity": "sha512-uX50CAM11tzhwswf0ftN0QfzW2FM3M4Mf/pD/nRRnmsTkcPTdMXVu4LHuLVTp4CMsaO+cOQlqgHXujHYfOIctg==", "dev": true, "license": "MIT", "dependencies": { - "@nrwl/devkit": "19.6.5", + "@nrwl/devkit": "19.8.3", "ejs": "^3.1.7", "enquirer": "~2.3.6", "ignore": "^5.0.4", @@ -5668,9 +5762,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.3", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-19.8.3.tgz", + "integrity": "sha512-ORHFFWMZcvFi0xcpCaXccXVEhFwAevSHOIKfW359+12H9w7VW2O42B+2NcVMK1mrDTOjlXTd+0AmAu7P4NzWFA==", "cpu": [ "arm64" ], @@ -5685,9 +5779,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.3", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-19.8.3.tgz", + "integrity": "sha512-Ji9DPA0tuzygMcypD/FHRDQSPipcRqMNmSaNKxVpcCbozVTWHvqXFk0rloDIUnxnE0+zvE9LN71H2sS4ZHdTQA==", "cpu": [ "x64" ], @@ -5702,9 +5796,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.3", + "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-19.8.3.tgz", + "integrity": "sha512-Ys+PqtBZCS+QBNs7he3fnxVhMWz/lSSaBVUlVHoQcV1Y4clEpP2TWNQSsbaVnnpcB7pdmKN5ymWdaCaAQuqCMw==", "cpu": [ "x64" ], @@ -5719,9 +5813,9 @@ } }, "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==", + "version": "19.8.3", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-19.8.3.tgz", + "integrity": "sha512-hGOlML60ELXkgkqLHB/w/sXbTbXFhOQGSXC72CjaP5G0u1gj8eTQKJ7WEsqPAFMk5SLFFxqM7eid0LmAYYuZWQ==", "cpu": [ "arm" ], @@ -5736,9 +5830,9 @@ } }, "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==", + "version": "19.8.3", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-19.8.3.tgz", + "integrity": "sha512-K/5iVbLbhsx28YtZHvveJgF41rbr2kMdabooZeFqy6VReN7U/zGJMjpV1FzDlf3TNr9jyjPDZgVQRS+qXau2qA==", "cpu": [ "arm64" ], @@ -5753,9 +5847,9 @@ } }, "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==", + "version": "19.8.3", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-19.8.3.tgz", + "integrity": "sha512-zqzWjFniZDXiI/3MYxbJ0yIenUKr56apLy70oABTBHx++dsUA3/DxLMNypMA82a8KQtsbePWUi3Pgtr+JIMNXw==", "cpu": [ "arm64" ], @@ -5770,9 +5864,9 @@ } }, "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==", + "version": "19.8.3", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-19.8.3.tgz", + "integrity": "sha512-W1RRCqsQvpur4BxP5g5cQwjZB6jhxYLSSXi3QQDaU5ITkaV5Pdj/L7D/G6YgRB8lzKZrXc57aLJ5UKY/Z+di7w==", "cpu": [ "x64" ], @@ -5787,9 +5881,9 @@ } }, "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==", + "version": "19.8.3", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-19.8.3.tgz", + "integrity": "sha512-waTo0zBBGnmU7fS87IpOnVGx7EHa0umzSMlGG0LUoU6swOeNODezsBn1Vbvaw1o7sStWBzdEBlxLxHOQXRAidg==", "cpu": [ "x64" ], @@ -5804,9 +5898,9 @@ } }, "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==", + "version": "19.8.3", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-19.8.3.tgz", + "integrity": "sha512-lio7ulblEMs1otMtVIrdfdMTBqKRZEHim57AcMHSVnwmtl2ENP6TR3YIgyigjfLlkPanNU7i0QQ4h6Nk2I/FRw==", "cpu": [ "arm64" ], @@ -5821,9 +5915,9 @@ } }, "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==", + "version": "19.8.3", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-19.8.3.tgz", + "integrity": "sha512-RU11iXJzdrw5CmogT2AwsjxK7g8vWf6Oy23NlrvsQFODtavjqAWoD5qpUY/H16s9lVDwrpzCbGbAXph0lbgLKA==", "cpu": [ "x64" ], @@ -6153,14 +6247,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.7", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.7.tgz", + "integrity": "sha512-WOBzO11qstznHbC9tZXQf6/8+PqmaRI6QYcdTspqXNh9q9nNglvi43Xn4tSIpEhW8aSHea9hgWZV8sG+i/4W9Q==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.6", - "@angular-devkit/schematics": "18.2.6", + "@angular-devkit/core": "18.2.7", + "@angular-devkit/schematics": "18.2.7", "jsonc-parser": "3.3.1" }, "engines": { @@ -6170,73 +6264,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.33.1", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.33.1.tgz", + "integrity": "sha512-TW6/r+Gl5jiXv54iK1xZ3mlVgTS/jaBp4vcQ0xGMdgiQ3WchEPcFSeYovL+YHT3tSud0GZqVtDQCz+5i76puqA==", "license": "MIT", "dependencies": { - "@sentry/core": "8.32.0", - "@sentry/types": "8.32.0", - "@sentry/utils": "8.32.0" + "@sentry/core": "8.33.1", + "@sentry/types": "8.33.1", + "@sentry/utils": "8.33.1" }, "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.33.1", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.33.1.tgz", + "integrity": "sha512-qauMRTm3qDaLqZ3ibI03cj4gLF40y0ij65nj+cns6iWxGCtPrO8tjvXFWuQsE7Aye9dGMnBgmv7uN+NTUtC3RA==", "license": "MIT", "dependencies": { - "@sentry/core": "8.32.0", - "@sentry/types": "8.32.0", - "@sentry/utils": "8.32.0" + "@sentry/core": "8.33.1", + "@sentry/types": "8.33.1", + "@sentry/utils": "8.33.1" }, "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.33.1", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.33.1.tgz", + "integrity": "sha512-fm4coIOjmanU29NOVN9MyaP4fUCOYytbtFqVSKRFNZQ/xAgNeySiBIbUd6IjujMmnOk9bY0WEUMcdm3Uotjdog==", "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.33.1", + "@sentry/core": "8.33.1", + "@sentry/types": "8.33.1", + "@sentry/utils": "8.33.1" }, "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.33.1", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.33.1.tgz", + "integrity": "sha512-nsxTFTPCT10Ty/v6+AiST3+yotGP1sUb8xqfKB9fPnS1hZHFryp0NnEls7xFjBsBbZPU1GpFkzrk/E6JFzixDQ==", "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.33.1", + "@sentry/core": "8.33.1", + "@sentry/types": "8.33.1", + "@sentry/utils": "8.33.1" }, "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.33.1", + "resolved": "https://registry.npmjs.org/@sentry/angular/-/angular-8.33.1.tgz", + "integrity": "sha512-jt4oViLMl/eqOALQmD0dPzXsy75Xp8amfRExgXoPdyDg6sLDNdEzpzrX2p7nGl7vsW/0Vm8NZ2TkbEBCll5wfQ==", "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.33.1", + "@sentry/core": "8.33.1", + "@sentry/types": "8.33.1", + "@sentry/utils": "8.33.1", "tslib": "^2.4.1" }, "engines": { @@ -6250,52 +6344,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.33.1", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.33.1.tgz", + "integrity": "sha512-c6zI/igexkLwZuGk+u8Rj26ChjxGgkhe6ZbKFsXCYaKAp5ep5X7HQRkkqgbxApiqlC0LduHdd/ymzh139JLg8w==", "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.33.1", + "@sentry-internal/feedback": "8.33.1", + "@sentry-internal/replay": "8.33.1", + "@sentry-internal/replay-canvas": "8.33.1", + "@sentry/core": "8.33.1", + "@sentry/types": "8.33.1", + "@sentry/utils": "8.33.1" }, "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.33.1", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.33.1.tgz", + "integrity": "sha512-3SS41suXLFzxL3OQvTMZ6q92ZapELVq2l2SoWlZopcamWhog2Ru0dp2vkunq97kFHb2TzKRTlFH4+4gbT8SJug==", "license": "MIT", "dependencies": { - "@sentry/types": "8.32.0", - "@sentry/utils": "8.32.0" + "@sentry/types": "8.33.1", + "@sentry/utils": "8.33.1" }, "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.33.1", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.33.1.tgz", + "integrity": "sha512-GjoAMvwtpIemoF/IiwZ7A60g4nQv3qwzR21GvJqDVUoKD0e8pv9OLX+HyXoUat4wEDGSuDUcUyUKD2G+od73QA==", "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.33.1", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.33.1.tgz", + "integrity": "sha512-uzuYpiiJuFY3N4WNHMBWUQX5oNv2t/TbG0OHRp3Rr7yeu+HSfD542TIp9/gMZ+G0Cxd8AmVO3wkKIFbk0TL4Qg==", "license": "MIT", "dependencies": { - "@sentry/types": "8.32.0" + "@sentry/types": "8.33.1" }, "engines": { "node": ">=14.18" @@ -6909,9 +7003,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 +7037,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.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz", + "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==", "dev": true, "license": "MIT", "dependencies": { @@ -6993,9 +7087,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 +7243,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.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.0.tgz", + "integrity": "sha512-wORFWjU30B2WJ/aXBfOm1LX9v9nyt9D3jsSOxC3cCaTQGCW5k4jNpmjFv3U7p/7s4yvdjHzwtv2Sd2dOyhjS0A==", "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.0", + "@typescript-eslint/type-utils": "8.8.0", + "@typescript-eslint/utils": "8.8.0", + "@typescript-eslint/visitor-keys": "8.8.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -7183,16 +7277,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.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.0.tgz", + "integrity": "sha512-uEFUsgR+tl8GmzmLjRqz+VrDv4eoaMqMXW7ruXfgThaAShO9JTciKpEsB+TvnfFfbg5IpujgMXVV36gOJRLtZg==", "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.0", + "@typescript-eslint/types": "8.8.0", + "@typescript-eslint/typescript-estree": "8.8.0", + "@typescript-eslint/visitor-keys": "8.8.0", "debug": "^4.3.4" }, "engines": { @@ -7212,14 +7306,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.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.0.tgz", + "integrity": "sha512-EL8eaGC6gx3jDd8GwEFEV091210U97J0jeEHrAYvIYosmEGet4wJ+g0SYmLu+oRiAwbSA5AVrt6DxLHfdd+bUg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.7.0", - "@typescript-eslint/visitor-keys": "8.7.0" + "@typescript-eslint/types": "8.8.0", + "@typescript-eslint/visitor-keys": "8.8.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7230,14 +7324,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.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.0.tgz", + "integrity": "sha512-IKwJSS7bCqyCeG4NVGxnOP6lLT9Okc3Zj8hLO96bpMkJab+10HIfJbMouLrlpyOr3yrQ1cA413YPFiGd1mW9/Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.7.0", - "@typescript-eslint/utils": "8.7.0", + "@typescript-eslint/typescript-estree": "8.8.0", + "@typescript-eslint/utils": "8.8.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -7255,9 +7349,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.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.0.tgz", + "integrity": "sha512-QJwc50hRCgBd/k12sTykOJbESe1RrzmX6COk8Y525C9l7oweZ+1lw9JiU56im7Amm8swlz00DRIlxMYLizr2Vw==", "dev": true, "license": "MIT", "engines": { @@ -7269,14 +7363,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.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.0.tgz", + "integrity": "sha512-ZaMJwc/0ckLz5DaAZ+pNLmHv8AMVGtfWxZe/x2JVEkD5LnmhWiQMMcYT7IY7gkdJuzJ9P14fRy28lUrlDSWYdw==", "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.0", + "@typescript-eslint/visitor-keys": "8.8.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -7298,16 +7392,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.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.0.tgz", + "integrity": "sha512-QE2MgfOTem00qrlPgyByaCHay9yb1+9BjnMFnSFkUKQfu7adBXDTnCAivURnuPPAG/qiB+kzKkZKmKfaMT0zVg==", "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.0", + "@typescript-eslint/types": "8.8.0", + "@typescript-eslint/typescript-estree": "8.8.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7321,13 +7415,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.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.0.tgz", + "integrity": "sha512-8mq51Lx6Hpmd7HnA2fcHQo3YgfX1qbccxQOgZcb4tvasu//zXRaA1j5ZRFeCw/VRAdFi4mRM9DnZw0Nu0Q2d1g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.7.0", + "@typescript-eslint/types": "8.8.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -8691,9 +8785,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.30001666", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001666.tgz", + "integrity": "sha512-gD14ICmoV5ZZM1OdzPWmpx+q4GyefaK06zi8hmfHV5xe4/2nOQX3+Dw5o+fSqOws2xVwL9j+anOPFwHzdEdV4g==", "funding": [ { "type": "opencollective", @@ -8851,9 +8945,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": { @@ -9218,9 +9312,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": { @@ -10090,19 +10184,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 +10497,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.32", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.32.tgz", + "integrity": "sha512-M+7ph0VGBQqqpTT2YrabjNKSQ2fEl9PVx6AK3N558gDH9NO8O6XN9SXXFWRo9u9PbEg/bWq+tjXQr+eXmxubCw==", "license": "ISC" }, "node_modules/emittery": { @@ -10699,9 +10780,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 +10791,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 +10803,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 +10815,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 +11236,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 +11360,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": { @@ -11350,15 +11429,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 +11447,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": { @@ -12048,31 +12127,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", @@ -13201,16 +13255,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 +14298,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 +15525,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 +15544,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 +15627,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 +15870,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", @@ -17589,22 +17613,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.3", + "resolved": "https://registry.npmjs.org/nx/-/nx-19.8.3.tgz", + "integrity": "sha512-/3FF4tgwPGRu4bV6O+aHqhTnOGHKF0/HNVkApUwjimSC+YzOX9VH1uBx2eReb4XC1scxDWkIzVi9gkFSXSQDjQ==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { "@napi-rs/wasm-runtime": "0.2.4", - "@nrwl/tao": "19.6.5", + "@nrwl/tao": "19.8.3", "@yarnpkg/lockfile": "^1.1.0", "@yarnpkg/parsers": "3.0.0-rc.46", "@zkochan/js-yaml": "0.0.7", @@ -17619,11 +17643,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 +17667,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.3", + "@nx/nx-darwin-x64": "19.8.3", + "@nx/nx-freebsd-x64": "19.8.3", + "@nx/nx-linux-arm-gnueabihf": "19.8.3", + "@nx/nx-linux-arm64-gnu": "19.8.3", + "@nx/nx-linux-arm64-musl": "19.8.3", + "@nx/nx-linux-x64-gnu": "19.8.3", + "@nx/nx-linux-x64-musl": "19.8.3", + "@nx/nx-win32-arm64-msvc": "19.8.3", + "@nx/nx-win32-x64-msvc": "19.8.3" }, "peerDependencies": { "@swc-node/register": "^1.8.0", @@ -17714,19 +17737,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 +17830,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 +18388,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", @@ -18885,9 +18892,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.166.1", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.166.1.tgz", + "integrity": "sha512-K8IpV8FJTCdwhsXFSbKj5vZ6IXNV079lukpG3cRtst2q5vMmUXRQiks7W3lOZLrjWyuJLKZDUiCeeDIUFORRuQ==", "license": "MIT", "dependencies": { "fflate": "^0.4.8", @@ -19439,16 +19446,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 +19463,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.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.11.0.tgz", + "integrity": "sha512-vTbzVAjQDzwQdKuvj7qEq6OlAprCjE656khuGQ4QaBLg7abQ9I9ISpmLuc6inWe7zP75AECjqUa4g4sdQvOXhg==", "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,9 +19895,9 @@ "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.4", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.4.tgz", + "integrity": "sha512-K0QDSNPXgyqO4GZq2HO5Q70TLxTH6cIT59RdoCHMivrC8rqzaTw5ab9prjz9KUN1El4FLXrBXJhik61JR4HcGg==", "dev": true, "license": "MIT", "dependencies": { @@ -19952,9 +19970,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": { @@ -21412,22 +21430,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.50", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.50.tgz", + "integrity": "sha512-q9GOap6q3KCsLMdOjXhWU5jVZ8/1dIib898JBRLsN+tBhENpBDcAVQbE0epADOjw11FhQQy9AcbqKGBQPUfTQA==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^6.1.47" + "tldts-core": "^6.1.50" }, "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.50", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.50.tgz", + "integrity": "sha512-na2EcZqmdA2iV9zHV7OHQDxxdciEpxrjbkp+aHmZgnZKHzoElLajP59np5/4+sare9fQBfixgvXKx8ev1d7ytw==", "dev": true, "license": "MIT" }, @@ -21945,9 +21963,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 +21982,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 +22815,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 +22834,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 +22842,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" diff --git a/package.json b/package.json index 03ab87fb7b80..dfe192b8d21b 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.7", + "@angular/cdk": "18.2.7", + "@angular/common": "18.2.7", + "@angular/compiler": "18.2.7", + "@angular/core": "18.2.7", + "@angular/forms": "18.2.7", + "@angular/localize": "18.2.7", + "@angular/material": "18.2.7", + "@angular/platform-browser": "18.2.7", + "@angular/platform-browser-dynamic": "18.2.7", + "@angular/router": "18.2.7", + "@angular/service-worker": "18.2.7", "@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.33.1", "@siemens/ngx-datatable": "22.4.1", "@swimlane/ngx-charts": "20.5.0", "@swimlane/ngx-graph": "8.4.0", @@ -63,7 +63,7 @@ "ngx-webstorage": "18.0.0", "papaparse": "5.4.1", "pdfjs-dist": "4.6.82", - "posthog-js": "1.165.0", + "posthog-js": "1.166.1", "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.7", "@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.7", + "@angular/compiler-cli": "18.2.7", + "@angular/language-service": "18.2.7", + "@sentry/types": "8.33.1", "@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.4", "@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.0", + "@typescript-eslint/parser": "8.8.0", + "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.4", "ts-jest": "29.2.5", "typescript": "5.5.4", "weak-napi": "2.0.2" 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/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/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/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/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/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/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/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/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/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/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/service/CourseService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java index 9e3b69f269cc..01bb68edc441 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 @@ -59,6 +59,7 @@ 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.notification.GroupNotification; +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.conversation.ConversationRepository; import de.tum.cit.aet.artemis.communication.service.notifications.GroupNotificationService; @@ -117,6 +118,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; @@ -210,7 +213,7 @@ 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, FaqRepository faqRepository) { this.courseRepository = courseRepository; this.exerciseService = exerciseService; this.exerciseDeletionService = exerciseDeletionService; @@ -250,6 +253,7 @@ public CourseService(CourseRepository courseRepository, ExerciseService exercise this.tutorialGroupChannelManagementService = tutorialGroupChannelManagementService; this.prerequisiteRepository = prerequisiteRepository; this.competencyRelationRepository = competencyRelationRepository; + this.faqRepository = faqRepository; } /** @@ -467,6 +471,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 +547,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/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/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..209f2a4fa040 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 @@ -1170,13 +1170,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); 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/exercise/web/ParticipationResource.java b/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java index 6559c28b9d93..c6cdc6ee1730 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 @@ -718,7 +718,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 +756,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 +931,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/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/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/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/plagiarism/service/ProgrammingPlagiarismDetectionService.java b/src/main/java/de/tum/cit/aet/artemis/plagiarism/service/ProgrammingPlagiarismDetectionService.java index a3c2cec8e9df..9fe9b1cc0f8c 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,12 @@ 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.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; @@ -313,8 +315,11 @@ private Language getJPlagProgrammingLanguage(ProgrammingExercise programmingExer case PYTHON -> new PythonLanguage(); case SWIFT -> new SwiftLanguage(); case KOTLIN -> new KotlinLanguage(); - default -> throw new BadRequestAlertException("Programming language " + programmingExercise.getProgrammingLanguage() + " not supported for plagiarism check.", - "ProgrammingExercise", "notSupported"); + case RUST -> new RustLanguage(); + case JAVASCRIPT -> new JavaScriptLanguage(); + case EMPTY, PHP, DART, HASKELL, ASSEMBLER, OCAML, C_SHARP, C_PLUS_PLUS, SQL, R, 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/ProgrammingExerciseBuildConfig.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExerciseBuildConfig.java index e8b5e554c37d..c36fc8f7d62b 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 @@ -25,6 +25,7 @@ @Entity @Table(name = "programming_exercise_build_config") @JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonIgnoreProperties(value = { "programmingExercise" }) public class ProgrammingExerciseBuildConfig extends DomainObject { private static final Logger log = LoggerFactory.getLogger(ProgrammingExerciseBuildConfig.class); @@ -58,7 +59,6 @@ public class ProgrammingExerciseBuildConfig extends DomainObject { private String dockerFlags; @OneToOne(mappedBy = "buildConfig") - @JsonIgnoreProperties("buildConfig") private ProgrammingExercise programmingExercise; @Column(name = "testwise_coverage_enabled") diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseBuildConfigRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseBuildConfigRepository.java index e543335002df..1c44860df2ee 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseBuildConfigRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseBuildConfigRepository.java @@ -35,4 +35,14 @@ default void generateBuildPlanAccessSecretIfNotExists(ProgrammingExerciseBuildCo default void loadAndSetBuildConfig(ProgrammingExercise programmingExercise) { programmingExercise.setBuildConfig(getProgrammingExerciseBuildConfigElseThrow(programmingExercise)); } + + /** + * Find a build config by its programming exercise's id and throw an Exception if it cannot be found + * + * @param programmingExerciseId of the programming exercise. + * @return The programming exercise related to the given id + */ + default ProgrammingExerciseBuildConfig findByExerciseIdElseThrow(long programmingExerciseId) { + return getValueElseThrow(findByProgrammingExerciseId(programmingExerciseId)); + } } 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..ab64ca6e53a8 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 @@ -226,26 +226,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 +327,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 +542,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/gitlabci/GitLabCIProgrammingLanguageFeatureService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/gitlabci/GitLabCIProgrammingLanguageFeatureService.java index 7cd996e35cc4..a92bcdd26cb5 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(RUST, new ProgrammingLanguageFeature(RUST, false, false, true, false, false, List.of(), false, false)); + programmingLanguageFeatures.put(JAVASCRIPT, new ProgrammingLanguageFeature(JAVASCRIPT, false, false, true, false, false, List.of(), false, false)); } } 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..38893ea41093 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 @@ -41,7 +41,7 @@ public JenkinsProgrammingLanguageFeatureService() { 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)); + programmingLanguageFeatures.put(RUST, new ProgrammingLanguageFeature(RUST, false, false, true, false, false, List.of(), false, false)); + programmingLanguageFeatures.put(JAVASCRIPT, new ProgrammingLanguageFeature(JAVASCRIPT, false, false, true, false, false, List.of(), false, false)); } } 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..525170cca334 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 @@ -49,7 +49,7 @@ public LocalCIProgrammingLanguageFeatureService() { 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(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(RUST, new ProgrammingLanguageFeature(RUST, false, false, true, false, false, List.of(), false, true)); + programmingLanguageFeatures.put(JAVASCRIPT, new ProgrammingLanguageFeature(JAVASCRIPT, false, false, true, 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..10569adbbf30 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 @@ -149,6 +149,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; 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..2222e4a5f3d9 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 @@ -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); 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 b8539e0d788d..05eb83b3005c 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 @@ -59,6 +59,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.enforceRoleInExercise.EnforceAtLeastStudentInExercise; import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInExercise.EnforceAtLeastTutorInExercise; import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; import de.tum.cit.aet.artemis.core.service.CourseService; @@ -72,6 +73,7 @@ import de.tum.cit.aet.artemis.plagiarism.service.PlagiarismDetectionConfigHelper; import de.tum.cit.aet.artemis.programming.domain.AuxiliaryRepository; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; +import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseBuildConfig; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseTestCase; import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage; import de.tum.cit.aet.artemis.programming.dto.BuildLogStatisticsDTO; @@ -79,6 +81,7 @@ import de.tum.cit.aet.artemis.programming.dto.ProgrammingExerciseResetOptionsDTO; import de.tum.cit.aet.artemis.programming.dto.ProgrammingExerciseTestCaseStateDTO; import de.tum.cit.aet.artemis.programming.repository.BuildLogStatisticsEntryRepository; +import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseRepository; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseTestCaseRepository; import de.tum.cit.aet.artemis.programming.repository.SolutionProgrammingExerciseParticipationRepository; @@ -115,6 +118,8 @@ public class ProgrammingExerciseResource { private final ProgrammingExerciseTestCaseRepository programmingExerciseTestCaseRepository; + private final ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository; + private final UserRepository userRepository; private final CourseService courseService; @@ -158,9 +163,9 @@ public class ProgrammingExerciseResource { private final Environment environment; public ProgrammingExerciseResource(ProgrammingExerciseRepository programmingExerciseRepository, ProgrammingExerciseTestCaseRepository programmingExerciseTestCaseRepository, - UserRepository userRepository, AuthorizationCheckService authCheckService, CourseService courseService, - Optional continuousIntegrationService, Optional versionControlService, ExerciseService exerciseService, - ExerciseDeletionService exerciseDeletionService, ProgrammingExerciseService programmingExerciseService, + ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository, UserRepository userRepository, AuthorizationCheckService authCheckService, + CourseService courseService, Optional continuousIntegrationService, Optional versionControlService, + ExerciseService exerciseService, ExerciseDeletionService exerciseDeletionService, ProgrammingExerciseService programmingExerciseService, ProgrammingExerciseRepositoryService programmingExerciseRepositoryService, ProgrammingExerciseTaskService programmingExerciseTaskService, StudentParticipationRepository studentParticipationRepository, StaticCodeAnalysisService staticCodeAnalysisService, GradingCriterionRepository gradingCriterionRepository, CourseRepository courseRepository, GitService gitService, AuxiliaryRepositoryService auxiliaryRepositoryService, @@ -171,6 +176,7 @@ public ProgrammingExerciseResource(ProgrammingExerciseRepository programmingExer this.programmingExerciseTaskService = programmingExerciseTaskService; this.programmingExerciseRepository = programmingExerciseRepository; this.programmingExerciseTestCaseRepository = programmingExerciseTestCaseRepository; + this.programmingExerciseBuildConfigRepository = programmingExerciseBuildConfigRepository; this.userRepository = userRepository; this.courseService = courseService; this.authCheckService = authCheckService; @@ -493,6 +499,21 @@ public ResponseEntity getProgrammingExercise(@PathVariable return ResponseEntity.ok().body(programmingExercise); } + /** + * GET /programming-exercises/:exerciseId/build-config : get the build config of "exerciseId" programmingExercise. + * + * @param exerciseId the id of the programmingExercise to retrieve the config for + * @return the ResponseEntity with status 200 (OK) and with body the programmingExerciseBuildConfig, or with status 404 (Not Found) + */ + @GetMapping("programming-exercises/{exerciseId}/build-config") + @EnforceAtLeastStudentInExercise + public ResponseEntity getBuildConfig(@PathVariable long exerciseId) { + log.debug("REST request to get build config of ProgrammingExercise : {}", exerciseId); + var buildConfig = programmingExerciseBuildConfigRepository.findByExerciseIdElseThrow(exerciseId); + + return ResponseEntity.ok().body(buildConfig); + } + /** * GET /programming-exercises/:exerciseId/with-participations/ : get the "exerciseId" programmingExercise. * 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/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/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/master.xml b/src/main/resources/config/liquibase/master.xml index 64295fe02504..49f3eeee4d63 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -23,6 +23,7 @@ + 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 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.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/webapp/app/core/auth/account.service.ts b/src/main/webapp/app/core/auth/account.service.ts index 93b66699ebba..3ed378a83525 100644 --- a/src/main/webapp/app/core/auth/account.service.ts +++ b/src/main/webapp/app/core/auth/account.service.ts @@ -380,4 +380,12 @@ export class AccountService implements IAccountService { const params = new HttpParams().set('participationId', participationId); return this.http.put('api/account/participation-vcs-access-token', null, { observe: 'response', params, responseType: 'text' as 'json' }); } + + /** + * Trades the current cookie for a new bearer token which is also able to authenticate the user. + * The Cookie stays valid, a new bearer token is generated on every call. + */ + rekeyCookieToBearerToken() { + return this.http.post('api/public/theia-token', null, { responseType: 'text' as 'json' }); + } } 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) {