diff --git a/build.gradle b/build.gradle index b59c9016479f..1cb102989859 100644 --- a/build.gradle +++ b/build.gradle @@ -15,16 +15,16 @@ plugins { id "idea" id "jacoco" id "org.springframework.boot" version "${spring_boot_version}" - id "io.spring.dependency-management" version "1.1.2" + id "io.spring.dependency-management" version "1.1.3" id "com.google.cloud.tools.jib" version "3.3.2" id "com.github.node-gradle.node" version "${gradle_node_plugin_version}" - id "com.diffplug.spotless" version "6.20.0" + id "com.diffplug.spotless" version "6.21.0" // this allows us to find outdated dependencies via ./gradlew dependencyUpdates - id "com.github.ben-manes.versions" version "0.47.0" + id "com.github.ben-manes.versions" version "0.48.0" id "com.github.andygoossens.modernizer" version "${modernizer_plugin_version}" id "com.gorylenko.gradle-git-properties" version "2.4.1" id "info.solidsoft.pitest" version "1.9.11" - id "org.owasp.dependencycheck" version "8.3.1" + id "org.owasp.dependencycheck" version "8.4.0" id "com.adarshr.test-logger" version "3.2.0" } @@ -122,7 +122,7 @@ tasks.register("testReport", TestReport) { } jacoco { - toolVersion = "0.8.8" + toolVersion = "0.8.10" } jar { @@ -263,15 +263,15 @@ dependencies { implementation "org.springdoc:springdoc-openapi-ui:1.7.0" implementation "com.vdurmont:semver4j:3.1.0" - implementation "com.github.docker-java:docker-java-core:3.3.2" - implementation "com.github.docker-java:docker-java-transport-httpclient5:3.3.2" + implementation "com.github.docker-java:docker-java-core:3.3.3" + implementation "com.github.docker-java:docker-java-transport-httpclient5:3.3.3" // import JHipster dependencies BOM implementation platform("tech.jhipster:jhipster-dependencies:${jhipster_dependencies_version}") 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.11.2" + implementation "io.micrometer:micrometer-registry-prometheus:1.11.4" implementation "net.logstash.logback:logstash-logback-encoder:7.4" implementation "com.fasterxml.jackson.datatype:jackson-datatype-hppc:${fasterxml_version}" implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${fasterxml_version}" @@ -288,7 +288,7 @@ dependencies { implementation "org.apache.commons:commons-math3:3.6.1" implementation "javax.transaction:javax.transaction-api:1.3" implementation "org.hibernate:hibernate-entitymanager:${hibernate_version}" - implementation "org.liquibase:liquibase-core:4.23.0" + implementation "org.liquibase:liquibase-core:4.23.2" implementation "org.springframework.boot:spring-boot-starter-validation:${spring_boot_version}" implementation "org.springframework.boot:spring-boot-loader-tools:${spring_boot_version}" implementation "org.springframework.boot:spring-boot-starter-mail:${spring_boot_version}" @@ -305,16 +305,16 @@ dependencies { implementation "org.springframework.boot:spring-boot-starter-thymeleaf:${spring_boot_version}" implementation "org.springframework.ldap:spring-ldap-core:2.4.1" - implementation "org.springframework.data:spring-data-ldap:2.7.14" + implementation "org.springframework.data:spring-data-ldap:2.7.15" implementation "org.springframework.cloud:spring-cloud-starter-netflix-eureka-client:3.1.7" implementation "org.springframework.cloud:spring-cloud-starter-config:3.1.8" implementation "org.springframework.boot:spring-boot-starter-cloud-connectors:2.2.13.RELEASE" - implementation "io.netty:netty-all:4.1.96.Final" - implementation "io.projectreactor.netty:reactor-netty:1.1.9" + implementation "io.netty:netty-all:4.1.97.Final" + implementation "io.projectreactor.netty:reactor-netty:1.1.11" implementation "org.springframework:spring-messaging:5.3.29" - implementation "org.springframework.retry:spring-retry:2.0.2" + implementation "org.springframework.retry:spring-retry:2.0.3" implementation "org.springframework.security:spring-security-config:${spring_security_version}" implementation "org.springframework.security:spring-security-data:${spring_security_version}" @@ -333,15 +333,15 @@ dependencies { implementation "io.springfox:springfox-bean-validators:3.0.0" implementation "mysql:mysql-connector-java:8.0.33" implementation "org.postgresql:postgresql:42.6.0" - implementation "com.h2database:h2:2.2.220" + implementation "com.h2database:h2:2.2.222" // zalando problem spring web can only be updated when we support Spring 6 implementation "org.zalando:problem-spring-web:0.27.0" implementation "com.ibm.icu:icu4j:73.2" implementation "com.github.seancfoley:ipaddress:5.4.0" - implementation "org.apache.maven:maven-model:3.9.3" - implementation "org.apache.pdfbox:pdfbox:2.0.29" - implementation "com.google.protobuf:protobuf-java:3.23.4" + implementation "org.apache.maven:maven-model:3.9.4" + implementation "org.apache.pdfbox:pdfbox:3.0.0" + implementation "com.google.protobuf:protobuf-java:3.24.3" implementation "org.apache.commons:commons-csv:1.10.0" implementation "org.commonmark:commonmark:0.21.0" implementation "commons-fileupload:commons-fileupload:1.5" @@ -366,14 +366,14 @@ dependencies { testImplementation "org.junit.jupiter:junit-jupiter:${junit_version}" testImplementation "org.mockito:mockito-core:${mockito_version}" testImplementation "org.mockito:mockito-junit-jupiter:${mockito_version}" - testImplementation "io.github.classgraph:classgraph:4.8.161" + testImplementation "io.github.classgraph:classgraph:4.8.162" testImplementation "org.awaitility:awaitility:4.2.0" testImplementation "org.apache.maven.shared:maven-invoker:3.2.0" - testImplementation "org.gradle:gradle-tooling-api:8.2.1" + testImplementation "org.gradle:gradle-tooling-api:8.3" testImplementation "org.apache.maven.surefire:surefire-report-parser:3.1.2" testImplementation "com.opencsv:opencsv:5.8" testImplementation "io.zonky.test:embedded-database-spring-test:2.3.0" - testImplementation "com.tngtech.archunit:archunit:1.0.1" + testImplementation "com.tngtech.archunit:archunit:1.1.0" testImplementation "org.skyscreamer:jsonassert:1.5.1" // Lightweight JSON library needed for the internals of the MockRestServiceServer @@ -417,7 +417,7 @@ tasks.withType(Test).configureEach { } wrapper { - gradleVersion = "8.2.1" + gradleVersion = "8.3" } tasks.register("stage") { diff --git a/gradle.properties b/gradle.properties index f78ac154d0fc..a3781fb28016 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,22 +7,22 @@ npm_version=9.6.0 # Dependency versions jhipster_dependencies_version=7.9.3 -spring_boot_version=2.7.14 +spring_boot_version=2.7.15 spring_security_version=5.7.10 hibernate_version=5.6.15.Final jaxb_runtime_version=4.0.3 -hazelcast_version=5.3.1 +hazelcast_version=5.3.2 junit_version=5.10.0 -mockito_version=5.4.0 +mockito_version=5.5.0 fasterxml_version=2.15.2 -jgit_version=6.6.0.202305301015-r -checkstyle_version=10.12.1 +jgit_version=6.7.0.202309050840-r +checkstyle_version=10.12.3 jplag_version=4.3.0 slf4j_version=1.7.36 -sentry_version=6.27.0 +sentry_version=6.29.0 # gradle plugin version -gradle_node_plugin_version=5.0.0 +gradle_node_plugin_version=7.0.0 apt_plugin_version=0.21 liquibase_plugin_version=2.1.1 modernizer_plugin_version=1.8.0 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9f4197d5f4b9..ac72c34e8acc 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/package-lock.json b/package-lock.json index 6ce1f04ecd2a..3ffa9dda7921 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,18 +10,18 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@angular/animations": "16.2.3", - "@angular/cdk": "16.2.2", - "@angular/common": "16.2.3", - "@angular/compiler": "16.2.3", - "@angular/core": "16.2.3", - "@angular/forms": "16.2.3", - "@angular/localize": "16.2.3", - "@angular/material": "16.2.2", - "@angular/platform-browser": "16.2.3", - "@angular/platform-browser-dynamic": "16.2.3", - "@angular/router": "16.2.3", - "@angular/service-worker": "16.2.3", + "@angular/animations": "16.2.5", + "@angular/cdk": "16.2.4", + "@angular/common": "16.2.5", + "@angular/compiler": "16.2.5", + "@angular/core": "16.2.5", + "@angular/forms": "16.2.5", + "@angular/localize": "16.2.5", + "@angular/material": "16.2.4", + "@angular/platform-browser": "16.2.5", + "@angular/platform-browser-dynamic": "16.2.5", + "@angular/router": "16.2.5", + "@angular/service-worker": "16.2.5", "@ctrl/ngx-emoji-mart": "9.2.0", "@danielmoncada/angular-datetime-picker": "16.0.1", "@fingerprintjs/fingerprintjs": "4.0.1", @@ -34,16 +34,16 @@ "@ng-bootstrap/ng-bootstrap": "15.1.1", "@ngx-translate/core": "15.0.0", "@ngx-translate/http-loader": "8.0.0", - "@sentry/angular-ivy": "7.66.0", - "@sentry/tracing": "7.66.0", - "@sentry/types": "7.66.0", + "@sentry/angular-ivy": "7.69.0", + "@sentry/tracing": "7.69.0", + "@sentry/types": "7.69.0", "@swimlane/ngx-charts": "20.4.1", "@swimlane/ngx-graph": "8.2.2", - "ace-builds": "1.24.1", + "ace-builds": "1.25.1", "bootstrap": "5.3.1", "brace": "0.11.1", "compare-versions": "6.1.0", - "core-js": "3.32.1", + "core-js": "3.32.2", "crypto-js": "4.1.1", "dayjs": "1.11.9", "diff-match-patch-typescript": "1.0.8", @@ -70,37 +70,37 @@ "split.js": "1.6.5", "ts-cacheable": "1.0.9", "tslib": "2.6.2", - "uuid": "9.0.0", + "uuid": "9.0.1", "webstomp-client": "1.2.6", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.0/xlsx-0.20.0.tgz", "zone.js": "0.13.0" }, "devDependencies": { "@angular-builders/jest": "16.0.1", - "@angular-eslint/builder": "16.1.1", - "@angular-eslint/eslint-plugin": "16.1.1", - "@angular-eslint/eslint-plugin-template": "16.1.1", - "@angular-eslint/schematics": "16.1.1", - "@angular-eslint/template-parser": "16.1.1", - "@angular/cli": "16.2.1", - "@angular/compiler-cli": "16.2.3", - "@angular/language-service": "16.2.3", - "@types/crypto-js": "4.1.1", + "@angular-eslint/builder": "16.1.2", + "@angular-eslint/eslint-plugin": "16.1.2", + "@angular-eslint/eslint-plugin-template": "16.1.2", + "@angular-eslint/schematics": "16.1.2", + "@angular-eslint/template-parser": "16.1.2", + "@angular/cli": "16.2.2", + "@angular/compiler-cli": "16.2.5", + "@angular/language-service": "16.2.5", + "@types/crypto-js": "4.1.2", "@types/d3-shape": "3.1.2", "@types/dompurify": "3.0.2", "@types/jest": "29.5.4", "@types/lodash-es": "4.17.9", - "@types/node": "20.5.7", + "@types/node": "20.6.0", "@types/papaparse": "5.3.8", "@types/showdown": "2.0.1", "@types/smoothscroll-polyfill": "0.3.1", "@types/sockjs-client": "1.5.1", "@types/uuid": "9.0.3", - "@typescript-eslint/eslint-plugin": "6.5.0", - "@typescript-eslint/parser": "6.5.0", - "eslint": "8.48.0", + "@typescript-eslint/eslint-plugin": "6.7.0", + "@typescript-eslint/parser": "6.7.0", + "eslint": "8.49.0", "eslint-config-prettier": "9.0.0", - "eslint-plugin-deprecation": "1.5.0", + "eslint-plugin-deprecation": "2.0.0", "eslint-plugin-jest": "27.2.3", "eslint-plugin-jest-extended": "2.0.0", "eslint-plugin-prettier": "5.0.0", @@ -116,7 +116,7 @@ "lint-staged": "14.0.1", "ng-mocks": "14.11.0", "prettier": "3.0.3", - "sass": "1.66.1", + "sass": "1.67.0", "ts-jest": "29.1.1", "typescript": "5.1.6", "weak-napi": "2.0.2", @@ -381,12 +381,12 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.2.1.tgz", - "integrity": "sha512-rXXO5zSI/iN6JtU3oU+vKfOB1N8n1iCH9aLudtJfO5zT9r29FIvV4YMmHO0iu78i4IhQAeJdr42cvrGPp8Y41A==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.2.2.tgz", + "integrity": "sha512-KeXIlibVrQEwIKbR9GViLKc3m1SXi/xuSXgIvSv+22FNu5i91ScsAhYLe65sDUL6m6MM1XQQMS46XN1Z9bRqQw==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.2.1", + "@angular-devkit/core": "16.2.2", "jsonc-parser": "3.2.0", "magic-string": "0.30.1", "ora": "5.4.1", @@ -398,10 +398,37 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.2.tgz", + "integrity": "sha512-6H4FsvP3rLJaGiWpIhCFPS7ZeNoM4sSrnFtRhhecu6s7MidzE4aqzuGdzJpzLammw1KL+DuTlN0gpLtM1Bvcwg==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "picomatch": "2.3.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, "node_modules/@angular-eslint/builder": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-16.1.1.tgz", - "integrity": "sha512-NaB/A0mmlzp7laiucRUsRyoCrOE1In3UifsGP0vD6yjUpefk4g0v+0vCg8mhsIky8gYDtBE9YRfUiLA9FlF/FA==", + "version": "16.1.2", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-16.1.2.tgz", + "integrity": "sha512-Y95IBEWqzWA7SyIh5nlPuFasw/4lOILrAdY5Ji6tOpIJgNFoiR9K1UcH46i34r3384ApN8GEQJ7FlK6D6qCOJA==", "dev": true, "dependencies": { "@nx/devkit": "16.5.1", @@ -413,18 +440,18 @@ } }, "node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-16.1.1.tgz", - "integrity": "sha512-TB01AWZBDfrZBxN1I50HfBXtC7q4NI5fwl1aS4tOfef2/kQjTtR9zmha8CsxjDkAOa9tA/4MUayAMqEBQLuHKQ==", + "version": "16.1.2", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-16.1.2.tgz", + "integrity": "sha512-wDiHPFsKTijMcQUPNcoHOJ5kezIPCCbmDK6LHH7hAdAC/eDY9NHL5e4zQ2Xkf3/r1PFuwVLGTwwreEHlmeENDw==", "dev": true }, "node_modules/@angular-eslint/eslint-plugin": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-16.1.1.tgz", - "integrity": "sha512-GauEwFGEcgIdsld4cVarFJYYxaRbMLzbpxyvBUDFg4LNjlcQNt7zfqXRLJoZAaFJFPtGtAoo1+6BlEKErsntuQ==", + "version": "16.1.2", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-16.1.2.tgz", + "integrity": "sha512-lYVvoKUIOg/ez15yfN4zY2A++vnIeJe1xh2ADNTmmjSh2PFV24K9YOgrTbgrY3Ul9kzGDTBkvYqslq+IvMGdIw==", "dev": true, "dependencies": { - "@angular-eslint/utils": "16.1.1", + "@angular-eslint/utils": "16.1.2", "@typescript-eslint/utils": "5.62.0" }, "peerDependencies": { @@ -433,13 +460,13 @@ } }, "node_modules/@angular-eslint/eslint-plugin-template": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-16.1.1.tgz", - "integrity": "sha512-hwbpiUxLIY3TnZycieh+G4fbTWGMfzKx076O5Vuh2H4ZfXfs6ZXoi3Z0TH6X9lTmdgrwzOg1v4o5kdqu7MqPBg==", + "version": "16.1.2", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-16.1.2.tgz", + "integrity": "sha512-2qsoUgPg9Qp4EVUJRwWcJ+8JMxBb0ma3pNBjFmY6LOd59igRYorJKfWep4Nln1EicYRDRsCLzeLHO976+b1yaQ==", "dev": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "16.1.1", - "@angular-eslint/utils": "16.1.1", + "@angular-eslint/bundled-angular-compiler": "16.1.2", + "@angular-eslint/utils": "16.1.2", "@typescript-eslint/type-utils": "5.62.0", "@typescript-eslint/utils": "5.62.0", "aria-query": "5.3.0", @@ -451,13 +478,13 @@ } }, "node_modules/@angular-eslint/schematics": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-16.1.1.tgz", - "integrity": "sha512-KlR01gpURPjz5OcoEvmKv3zi8l6lFpXYmqkXbGMCz828QlqBz1X7iGLAPJki+WUFSFKbRsf4qqaWq6O/8vph7Q==", + "version": "16.1.2", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-16.1.2.tgz", + "integrity": "sha512-319i47NU6nfaAaQTQYN7k320proTIBCueWGt+fbT11210CMqQriFmD+B85AatCwQgMgLd8Rhs1/F7YL2OOhegA==", "dev": true, "dependencies": { - "@angular-eslint/eslint-plugin": "16.1.1", - "@angular-eslint/eslint-plugin-template": "16.1.1", + "@angular-eslint/eslint-plugin": "16.1.2", + "@angular-eslint/eslint-plugin-template": "16.1.2", "@nx/devkit": "16.5.1", "ignore": "5.2.4", "nx": "16.5.1", @@ -469,12 +496,12 @@ } }, "node_modules/@angular-eslint/template-parser": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-16.1.1.tgz", - "integrity": "sha512-ZJ+M4+JGYcsIP/t+XiuzL5A5pCjjCen272U3/M/WqIMDDxyIKrHubK1bVtr2kndCEudqud+WyJU0ub13UIwGgw==", + "version": "16.1.2", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-16.1.2.tgz", + "integrity": "sha512-vIkPOShVJLBEHYY3jISCVvJF3lXL//Y70J8T9lY2CBowgqp6AzzJ6cZU7JxrORN6b64rBUVvUtCGo8L36GvfuA==", "dev": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "16.1.1", + "@angular-eslint/bundled-angular-compiler": "16.1.2", "eslint-scope": "^7.0.0" }, "peerDependencies": { @@ -483,12 +510,12 @@ } }, "node_modules/@angular-eslint/utils": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-16.1.1.tgz", - "integrity": "sha512-cmSTyFFY2TMLjhKdju0KQ9GB6nnXt1AbY9tZ0UtWGo3NKbrBUogc+PR9ma17VRAGhvdj/sSVkStphJH3F7rUgQ==", + "version": "16.1.2", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-16.1.2.tgz", + "integrity": "sha512-2yfEK3BPSJsUhP4JCz0EB6ktu4E4+/zc9qdtZvPWNF/eww2J/oYVPjY47C/HVg4MXpjJTI8vbdkvcnxrICIkfw==", "dev": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "16.1.1", + "@angular-eslint/bundled-angular-compiler": "16.1.2", "@typescript-eslint/utils": "5.62.0" }, "peerDependencies": { @@ -497,9 +524,9 @@ } }, "node_modules/@angular/animations": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-16.2.3.tgz", - "integrity": "sha512-MEjCWjN7RcHNFHkDYB3ZvEQqt94EzwevVXfld6rcOZNwJxcOVyi7+nQQ1YhWLPSW81HF76bpwD3RWWhZpKdXQQ==", + "version": "16.2.5", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-16.2.5.tgz", + "integrity": "sha512-2reD50S9zWvhewRvwl320iuRICN9s0fI+3nKULlwcyJ0praLRhJ1SnaAK3NEEu7MWo3n9sb3iVTzA6S9qZRJ4g==", "dependencies": { "tslib": "^2.3.0" }, @@ -507,13 +534,13 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/core": "16.2.3" + "@angular/core": "16.2.5" } }, "node_modules/@angular/cdk": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-16.2.2.tgz", - "integrity": "sha512-luUmeIFuEX4N3EOLhg1DM2hgsR+Is1Qd0a5xflbo30hZFnufppyzjaOvljNYUFtNTD9BaQRXaZDFA2cyTgfzZw==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-16.2.4.tgz", + "integrity": "sha512-Hnh7Gs+gAkBnRYIMkDXRElEPAmBFas37isIfOtiqEmkgmSPFxsPpDOXK1soXeDk8U+yNmDWnO0fcHPp/pobHCw==", "dependencies": { "tslib": "^2.3.0" }, @@ -527,15 +554,15 @@ } }, "node_modules/@angular/cli": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.2.1.tgz", - "integrity": "sha512-nuCc0VOGjuUFQo1Pu9CyFQ4VTy7OuwTiwxOG9qbut4FSGz2CO9NeqoamPUuy6rpKVu5JxVe+L6Y4OFaNKv2n3Q==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.2.2.tgz", + "integrity": "sha512-PmhR/NMVVCiATXxHLkVCV781Q5aa5DaYye9+plZGX3rdKTilEunRNIfT13w7IuRfa0K/pKZj6PJU1S6yb7sqZg==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1602.1", - "@angular-devkit/core": "16.2.1", - "@angular-devkit/schematics": "16.2.1", - "@schematics/angular": "16.2.1", + "@angular-devkit/architect": "0.1602.2", + "@angular-devkit/core": "16.2.2", + "@angular-devkit/schematics": "16.2.2", + "@schematics/angular": "16.2.2", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", "ini": "4.1.1", @@ -560,10 +587,52 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular/cli/node_modules/@angular-devkit/architect": { + "version": "0.1602.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1602.2.tgz", + "integrity": "sha512-JFIeKKW7V2+/C8+pTReM6gfQkVU9l1IR1OCb9vvHWTRvuTr7E5h2L1rUInnmLiRWkEvkYfG29B+UPpYlkVl9oQ==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "16.2.2", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/core": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.2.tgz", + "integrity": "sha512-6H4FsvP3rLJaGiWpIhCFPS7ZeNoM4sSrnFtRhhecu6s7MidzE4aqzuGdzJpzLammw1KL+DuTlN0gpLtM1Bvcwg==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "picomatch": "2.3.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, "node_modules/@angular/common": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-16.2.3.tgz", - "integrity": "sha512-hOC2yqISBRAzltuVJQ3CEJxHRp9mWggysp0or5HydbcmvB6WIroECL7U0u36VA95zC9SXnymHA13OwiFPpmahA==", + "version": "16.2.5", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-16.2.5.tgz", + "integrity": "sha512-MCPSZfPXTEqdkswPczivwjqV117YeVjObtyxZsDAwrTZHzYBtfQreQG1XJ1IRRgDncznP6ke0mdH9LyD2LgZKQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -571,14 +640,14 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/core": "16.2.3", + "@angular/core": "16.2.5", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-16.2.3.tgz", - "integrity": "sha512-bFc7YRHNdBJZD2HiORBQun2p40emvEt8D4JwXnW1JIStAWKJOXLyEjx045wNddqH7NpUq8AE2F1i82hIDNQZ1g==", + "version": "16.2.5", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-16.2.5.tgz", + "integrity": "sha512-DpLfWWZFk4lbr81W7sLRt15+/nbyyqTvz+UmGcrSfKBTSbV0VSoUjC3XZeIdPWoIgQXiKUCpaC0YXw0BjaOl0g==", "dependencies": { "tslib": "^2.3.0" }, @@ -586,7 +655,7 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/core": "16.2.3" + "@angular/core": "16.2.5" }, "peerDependenciesMeta": { "@angular/core": { @@ -595,9 +664,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.2.3.tgz", - "integrity": "sha512-4p1tDeeONiq/zceC0T6unXDuqyWiAe7v2Ag7+ewwM9V8BF+YOEpEI/41lxzmbK2U1YUvG3jWfZyw3ertQlMp0Q==", + "version": "16.2.5", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.2.5.tgz", + "integrity": "sha512-6TtyFxro4iukVXhLlzxz7sVCMfAlNQhSYnizIJRSW31uQ0Uku8rjlUmX1tCAmhW6CacLumiz2tcy04Xn/QFWyw==", "dependencies": { "@babel/core": "7.22.5", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -617,7 +686,7 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/compiler": "16.2.3", + "@angular/compiler": "16.2.5", "typescript": ">=4.9.3 <5.2" } }, @@ -651,9 +720,9 @@ } }, "node_modules/@angular/core": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-16.2.3.tgz", - "integrity": "sha512-YCzm7Rd2l0Ti0dZ1Mw3OfoQqlLolDN6jBEPy9Ah1s/KB+jKwNK9An3g8A9H6/jQIFwHCtxRad3LYH5ftknNMBQ==", + "version": "16.2.5", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-16.2.5.tgz", + "integrity": "sha512-Po2LMUnPg23D2qI7EYaoA4x3lRswx9nxfpwROzfFPbMNJ3JVbTK0HkTD2dFPGxRua2UjfJTb1um23tEGO4OGMQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -666,9 +735,9 @@ } }, "node_modules/@angular/forms": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-16.2.3.tgz", - "integrity": "sha512-d2ELs3PU4o1Yb89w4X3trD3CFWrDUsuFKs1hyNSYPWqCmcQ+tAfr9mizYPTVPSvee/RPRBqDEa0YTzfVpOvX4Q==", + "version": "16.2.5", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-16.2.5.tgz", + "integrity": "sha512-iYJImRji1OiYIcC2mDBcXhtvPfAoEGT+HqZpivu+/ZPLuf+QegC9+ktJw90SQXR+xccmpkUb9MsJ52SN2MgkPA==", "dependencies": { "tslib": "^2.3.0" }, @@ -676,25 +745,25 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "16.2.3", - "@angular/core": "16.2.3", - "@angular/platform-browser": "16.2.3", + "@angular/common": "16.2.5", + "@angular/core": "16.2.5", + "@angular/platform-browser": "16.2.5", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/language-service": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-16.2.3.tgz", - "integrity": "sha512-AFBq643tXGSoBUFNd0c1vJzReehtrqUZCzCRV3Gv5FNgb5xTfNdh3txIK4Cz/XhdSou+f2Yq6keFDlj2RYnlmA==", + "version": "16.2.5", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-16.2.5.tgz", + "integrity": "sha512-lYNRN4+iavDuAs86lRHuiTUxtVtsarCZPeoG6K1TEvrXrvmIbTtAbvONNMMnteO9ltCTduyREF9/sefE2Qw/Rg==", "dev": true, "engines": { "node": "^16.14.0 || >=18.10.0" } }, "node_modules/@angular/localize": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-16.2.3.tgz", - "integrity": "sha512-8dw4Vf3lqwQK4RBzBjYVNNoXlMJAyEF3pREqQo5aWcGTbBOAwM7SQuRQfxM8yjZZaXcKQEv/XJfEym5UVzGaUw==", + "version": "16.2.5", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-16.2.5.tgz", + "integrity": "sha512-vDtrBlbWOqtATqaBv3gmxBT0e8TfxwW+4J47S8u5Pbi1ZAnQfDkD9MNivC6/CAifFMcxN1pH8NALwLXOUga1PA==", "dependencies": { "@babel/core": "7.22.5", "fast-glob": "3.3.0", @@ -709,8 +778,8 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/compiler": "16.2.3", - "@angular/compiler-cli": "16.2.3" + "@angular/compiler": "16.2.5", + "@angular/compiler-cli": "16.2.5" } }, "node_modules/@angular/localize/node_modules/@babel/core": { @@ -758,9 +827,9 @@ } }, "node_modules/@angular/material": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-16.2.2.tgz", - "integrity": "sha512-0SaBPZsZ1jxq5yJeey+V2k7nq1Izw63fjxkyLx7rCcdowwwoBnG/dZsY97/5Qs2cZX0J+Z0iNpMYVJZd72GsvQ==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-16.2.4.tgz", + "integrity": "sha512-TIZ/0MKObn5YU9n/VReghJJKqgkqyzrWVNEJ8UgOP6MV5o+kAbqLSmlDJEyjLIwJF0vPnJ3UP6qbEOfEi1OLaA==", "dependencies": { "@material/animation": "15.0.0-canary.bc9ae6c9c.0", "@material/auto-init": "15.0.0-canary.bc9ae6c9c.0", @@ -813,7 +882,7 @@ }, "peerDependencies": { "@angular/animations": "^16.0.0 || ^17.0.0", - "@angular/cdk": "16.2.2", + "@angular/cdk": "16.2.4", "@angular/common": "^16.0.0 || ^17.0.0", "@angular/core": "^16.0.0 || ^17.0.0", "@angular/forms": "^16.0.0 || ^17.0.0", @@ -822,9 +891,9 @@ } }, "node_modules/@angular/platform-browser": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-16.2.3.tgz", - "integrity": "sha512-adWINGgjIMxwbWJhkMwpEfb4FRFMda5X6ahxWQX2E03Nl0kzePI6cvlJqAgp+iBwTkieWeU8BThJk2/rMkS3bw==", + "version": "16.2.5", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-16.2.5.tgz", + "integrity": "sha512-p+1GH/M4Vwoyp7brKkNBcMTxscoZxA1zehetFlNr8kArXWiISgPolyqOVzvT6cycYKu5uSRLnvHOTDss6xrAuA==", "dependencies": { "tslib": "^2.3.0" }, @@ -832,9 +901,9 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/animations": "16.2.3", - "@angular/common": "16.2.3", - "@angular/core": "16.2.3" + "@angular/animations": "16.2.5", + "@angular/common": "16.2.5", + "@angular/core": "16.2.5" }, "peerDependenciesMeta": { "@angular/animations": { @@ -843,9 +912,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.2.3.tgz", - "integrity": "sha512-Y3cYob1VGzT1xSMbuLGVxPlyuhv4zshYEo/yy2626YD63DigqYwGzj+gT0JoU1eNuXw2UWp3R67d9F8SC015Jw==", + "version": "16.2.5", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.2.5.tgz", + "integrity": "sha512-kzC4z/KmLss8Du9uM1Q16r+3EqDExKKHnrb3G3tuEQ1jTvYCysdWoooVSBmtIlQUw13znpBm1B7XLoyviFvnwA==", "dependencies": { "tslib": "^2.3.0" }, @@ -853,16 +922,16 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "16.2.3", - "@angular/compiler": "16.2.3", - "@angular/core": "16.2.3", - "@angular/platform-browser": "16.2.3" + "@angular/common": "16.2.5", + "@angular/compiler": "16.2.5", + "@angular/core": "16.2.5", + "@angular/platform-browser": "16.2.5" } }, "node_modules/@angular/router": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-16.2.3.tgz", - "integrity": "sha512-xjF5v6BzXanPB0VoIxeKXg1DO95nKJ9UjTsmB5ZOufDcqQXE81NAnH7iEKOymvU7aacqrgD467vcDtGNWJdfQQ==", + "version": "16.2.5", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-16.2.5.tgz", + "integrity": "sha512-5IXhe6G7zYFUwHSfUgPw+I/q6M1AcfSyaOVcjMFQ94bVSWEMq5KrGCDc8HQtkdw7GqJ4txwbyQKSKp7khpqShQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -870,16 +939,16 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "16.2.3", - "@angular/core": "16.2.3", - "@angular/platform-browser": "16.2.3", + "@angular/common": "16.2.5", + "@angular/core": "16.2.5", + "@angular/platform-browser": "16.2.5", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/service-worker": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-16.2.3.tgz", - "integrity": "sha512-CKBtxA7oHbozP22hD15ZdGeh4Wl4BqNRG93O1RI74zEY2Nq0UgDc/rDkEtfe0bGWLBhU+q5q/6M+DPSy6erqzA==", + "version": "16.2.5", + "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-16.2.5.tgz", + "integrity": "sha512-rHSFkrzyOunWwAQNtTC01ry2inrutlCad8MChK+fHCAhD2maWbNHtIelXR5ylojx7EyTUY0TPL30D60z2mXbwA==", "dependencies": { "tslib": "^2.3.0" }, @@ -890,8 +959,8 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "16.2.3", - "@angular/core": "16.2.3" + "@angular/common": "16.2.5", + "@angular/core": "16.2.5" } }, "node_modules/@assemblyscript/loader": { @@ -976,25 +1045,25 @@ } }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.10.tgz", - "integrity": "sha512-Av0qubwDQxC56DoUReVDeLfMEjYYSN1nZrTUrWkXd7hpU73ymRANkbuDm3yni9npkn+RXy9nNbEJZEzXr7xrfQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", + "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", "dev": true, "peer": true, "dependencies": { - "@babel/types": "^7.22.10" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.10.tgz", - "integrity": "sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", "dependencies": { "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.5", + "@babel/helper-validator-option": "^7.22.15", "browserslist": "^4.21.9", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -1004,16 +1073,16 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.11.tgz", - "integrity": "sha512-y1grdYL4WzmUDBRGK0pDbIoFd7UZKoDurDzWEoNMYoj1EL+foGRQNyPWDcC+YyegN5y1DUsFFmzjGijB3nSVAQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", + "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", "dev": true, "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.5", "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.15", "@babel/helper-optimise-call-expression": "^7.22.5", "@babel/helper-replace-supers": "^7.22.9", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", @@ -1028,9 +1097,9 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.9.tgz", - "integrity": "sha512-+svjVa/tFwsNSG4NEy1h85+HQ5imbT92Q5/bgtS7P0GTQlP8WuFdqsiABmQouhiFGyV66oGxZFpeYHza1rNsKw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", + "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", "dev": true, "peer": true, "dependencies": { @@ -1094,39 +1163,39 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz", - "integrity": "sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.15.tgz", + "integrity": "sha512-qLNsZbgrNh0fDQBCPocSL8guki1hcPvltGDv/NxvUoABwFq7GkKSu1nRXeJkVZc+wJvne2E0RKQz+2SQrz6eAA==", "dev": true, "peer": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", - "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz", - "integrity": "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==", + "version": "7.22.17", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.17.tgz", + "integrity": "sha512-XouDDhQESrLHTpnBtCKExJdyY4gJCdrvH2Pyv8r8kovX2U8G0dRUOT45T9XlbLtuu9CLXP15eusnkprhoPV5iQ==", "dependencies": { "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-module-imports": "^7.22.15", "@babel/helper-simple-access": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.5" + "@babel/helper-validator-identifier": "^7.22.15" }, "engines": { "node": ">=6.9.0" @@ -1157,15 +1226,15 @@ } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.9.tgz", - "integrity": "sha512-8WWC4oR4Px+tr+Fp0X3RHDVfINGpF3ad1HIbrc8A77epiR6eMMc6jsgozkzT2uDiOOdoS9cLIQ+XD2XvI2WSmQ==", + "version": "7.22.17", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.17.tgz", + "integrity": "sha512-bxH77R5gjH3Nkde6/LuncQoLaP16THYPscurp1S8z7S9ZgezCyV3G8Hc+TZiCmY8pz4fp8CvKSgtJMW0FkLAxA==", "dev": true, "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-wrap-function": "^7.22.9" + "@babel/helper-wrap-function": "^7.22.17" }, "engines": { "node": ">=6.9.0" @@ -1236,44 +1305,72 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.15.tgz", + "integrity": "sha512-4E/F9IIEi8WR94324mbDUMo074YTheJmd7eZF5vITTeYchqAi6sYXRLHUVsmkdmY4QjfKTcB2jB7dVP3NaBElQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", - "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", + "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.10.tgz", - "integrity": "sha512-OnMhjWjuGYtdoO3FmsEFWvBStBAe2QOgwOLsLNDjN+aaiMD8InJk1/O3HSD8lkqTjCgg5YI34Tz15KNNA3p+nQ==", + "version": "7.22.17", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.17.tgz", + "integrity": "sha512-nAhoheCMlrqU41tAojw9GpVEKDlTS8r3lzFmF0lP52LwblCPbuFSO7nGIZoIcoU5NIm1ABrna0cJExE4Ay6l2Q==", "dev": true, "peer": true, "dependencies": { "@babel/helper-function-name": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.10" + "@babel/template": "^7.22.15", + "@babel/types": "^7.22.17" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function/node_modules/@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.11.tgz", - "integrity": "sha512-vyOXC8PBWaGc5h7GMsNx68OH33cypkEDJCHvYVVgVbbxJDROYVtexSk0gK5iCF1xNjRIN2s8ai7hwkWDq5szWg==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.15.tgz", + "integrity": "sha512-7pAjK0aSdxOwR+CcYAqgWOGy5dcfvzsTIfFTb2odQqW47MDfv14UaJDY6eng8ylM2EaeKXdxaSWESbkmaQHTmw==", "dependencies": { - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.11", - "@babel/types": "^7.22.11" + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers/node_modules/@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" @@ -1293,9 +1390,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.14", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.14.tgz", - "integrity": "sha512-1KucTHgOvaw/LzCVrEOAyXkr9rQlp0A1HiHRYnSUE9dmb8PvPW7o5sscg+5169r54n3vGlbx6GevTE/Iw/P3AQ==", + "version": "7.22.16", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz", + "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==", "bin": { "parser": "bin/babel-parser.js" }, @@ -1304,9 +1401,9 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.22.5", - "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.22.5.tgz", - "integrity": "sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==", + "version": "7.22.15", + "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.22.15.tgz", + "integrity": "sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==", "dev": true, "peer": true, "dependencies": { @@ -1320,15 +1417,15 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz", - "integrity": "sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz", + "integrity": "sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==", "dev": true, "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.5" + "@babel/plugin-transform-optional-chaining": "^7.22.15" }, "engines": { "node": ">=6.9.0" @@ -1341,6 +1438,7 @@ "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead.", "dev": true, "peer": true, "dependencies": { @@ -1373,6 +1471,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead.", "dev": true, "peer": true, "dependencies": { @@ -1686,9 +1785,9 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.11.tgz", - "integrity": "sha512-0pAlmeRJn6wU84zzZsEOx1JV1Jf8fqO9ok7wofIJwUnplYo247dcd24P+cMJht7ts9xkzdtB0EPHmOb7F+KzXw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.15.tgz", + "integrity": "sha512-jBm1Es25Y+tVoTi5rfd5t1KLmL8ogLKpXszboWOTTtGFGz2RKnQe2yn7HbZ+kb/B8N0FVSGQo874NSlOU1T4+w==", "dev": true, "peer": true, "dependencies": { @@ -1739,9 +1838,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.10.tgz", - "integrity": "sha512-1+kVpGAOOI1Albt6Vse7c8pHzcZQdQKW+wJH+g8mCaszOdDVwRXa/slHPqIw+oJAJANTKDMuM2cBdV0Dg618Vg==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.15.tgz", + "integrity": "sha512-G1czpdJBZCtngoK1sJgloLiOHUnkb/bLZwqVZD8kXmq0ZnVfTTWUcs9OWtp0mBtYJ+4LQY1fllqBkOIPhXmFmw==", "dev": true, "peer": true, "dependencies": { @@ -1790,19 +1889,19 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.6.tgz", - "integrity": "sha512-58EgM6nuPNG6Py4Z3zSuu0xWu2VfodiMi72Jt5Kj2FECmaYk1RrTXA45z6KBFsu9tRgwQDwIiY4FXTt+YsSFAQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz", + "integrity": "sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==", "dev": true, "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-compilation-targets": "^7.22.15", "@babel/helper-environment-visitor": "^7.22.5", "@babel/helper-function-name": "^7.22.5", "@babel/helper-optimise-call-expression": "^7.22.5", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", "@babel/helper-split-export-declaration": "^7.22.6", "globals": "^11.1.0" }, @@ -1831,9 +1930,9 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.10.tgz", - "integrity": "sha512-dPJrL0VOyxqLM9sritNbMSGx/teueHF/htMKrPT7DNxccXxRDPYqlgPFFdr8u+F+qUZOkZoXue/6rL5O5GduEw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.15.tgz", + "integrity": "sha512-HzG8sFl1ZVGTme74Nw+X01XsUTqERVQ6/RLHo3XjGRzm7XD6QTtfS3NJotVgCGy8BzkDqRjRBD8dAyJn5TuvSQ==", "dev": true, "peer": true, "dependencies": { @@ -1931,9 +2030,9 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz", - "integrity": "sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz", + "integrity": "sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==", "dev": true, "peer": true, "dependencies": { @@ -2048,13 +2147,13 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.11.tgz", - "integrity": "sha512-o2+bg7GDS60cJMgz9jWqRUsWkMzLCxp+jFDeDUT5sjRlAxcJWZ2ylNdI7QQ2+CH5hWu7OnN+Cv3htt7AkSf96g==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.15.tgz", + "integrity": "sha512-jWL4eh90w0HQOTKP2MoXXUpVxilxsB2Vl4ji69rSjS3EcZ/v4sBmn+A3NpepuJzBhOaEBbR7udonlHHn5DWidg==", "dev": true, "peer": true, "dependencies": { - "@babel/helper-module-transforms": "^7.22.9", + "@babel/helper-module-transforms": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-simple-access": "^7.22.5" }, @@ -2169,17 +2268,17 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.11.tgz", - "integrity": "sha512-nX8cPFa6+UmbepISvlf5jhQyaC7ASs/7UxHmMkuJ/k5xSHvDPPaibMo+v3TXwU/Pjqhep/nFNpd3zn4YR59pnw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz", + "integrity": "sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==", "dev": true, "peer": true, "dependencies": { "@babel/compat-data": "^7.22.9", - "@babel/helper-compilation-targets": "^7.22.10", + "@babel/helper-compilation-targets": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.22.5" + "@babel/plugin-transform-parameters": "^7.22.15" }, "engines": { "node": ">=6.9.0" @@ -2223,9 +2322,9 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.22.12", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.12.tgz", - "integrity": "sha512-7XXCVqZtyFWqjDsYDY4T45w4mlx1rf7aOgkc/Ww76xkgBiOlmjPkx36PBLHa1k1rwWvVgYMPsbuVnIamx2ZQJw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.15.tgz", + "integrity": "sha512-ngQ2tBhq5vvSJw2Q2Z9i7ealNkpDMU0rGWnHPKqRZO0tzZ5tlaoz4hDvhXioOoaE0X2vfNss1djwg0DXlfu30A==", "dev": true, "peer": true, "dependencies": { @@ -2241,9 +2340,9 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz", - "integrity": "sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz", + "integrity": "sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==", "dev": true, "peer": true, "dependencies": { @@ -2654,18 +2753,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.11.tgz", - "integrity": "sha512-mzAenteTfomcB7mfPtyi+4oe5BZ6MXxWcn4CX+h4IRJ+OOGXBrWU6jDQavkQI9Vuc5P+donFabBfFCcmWka9lQ==", + "version": "7.22.17", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.17.tgz", + "integrity": "sha512-xK4Uwm0JnAMvxYZxOVecss85WxTEIbTa7bnGyf/+EgCL5Zt3U7htUpEOWv9detPlamGKuRzCqw74xVglDWpPdg==", "dependencies": { - "@babel/code-frame": "^7.22.10", - "@babel/generator": "^7.22.10", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.22.15", "@babel/helper-environment-visitor": "^7.22.5", "@babel/helper-function-name": "^7.22.5", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.11", - "@babel/types": "^7.22.11", + "@babel/parser": "^7.22.16", + "@babel/types": "^7.22.17", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -2674,11 +2773,11 @@ } }, "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.10.tgz", - "integrity": "sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.15.tgz", + "integrity": "sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==", "dependencies": { - "@babel/types": "^7.22.10", + "@babel/types": "^7.22.15", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -2688,12 +2787,12 @@ } }, "node_modules/@babel/types": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.11.tgz", - "integrity": "sha512-siazHiGuZRz9aB9NpHy9GOs9xiQPKnMzgdr493iI1M67vRXpnEq8ZOOKzezC5q7zwuQ6sDhdSp4SD9ixKSqKZg==", + "version": "7.22.17", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.17.tgz", + "integrity": "sha512-YSQPHLFtQNE5xN9tHuZnzu8vPr61wVTBZdfv1meex1NBosa4iT05k/Jw06ddJugi4bk7The/oSwQGFcksmEJQg==", "dependencies": { "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.15", "to-fast-properties": "^2.0.0" }, "engines": { @@ -3229,9 +3328,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.48.0.tgz", - "integrity": "sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz", + "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -4214,6 +4313,14 @@ "uuid": "9.0.0" } }, + "node_modules/@ls1intum/apollon/node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@material/animation": { "version": "15.0.0-canary.bc9ae6c9c.0", "resolved": "https://registry.npmjs.org/@material/animation/-/animation-15.0.0-canary.bc9ae6c9c.0.tgz", @@ -5250,6 +5357,150 @@ "node": ">= 10" } }, + "node_modules/@nx/nx-darwin-x64": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-16.5.1.tgz", + "integrity": "sha512-j9HmL1l8k7EVJ3eOM5y8COF93gqrydpxCDoz23ZEtsY+JHY77VAiRQsmqBgEx9GGA2dXi9VEdS67B0+1vKariw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-freebsd-x64": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-16.5.1.tgz", + "integrity": "sha512-CXSPT01aVS869tvCCF2tZ7LnCa8l41wJ3mTVtWBkjmRde68E5Up093hklRMyXb3kfiDYlfIKWGwrV4r0eH6x1A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-arm-gnueabihf": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-16.5.1.tgz", + "integrity": "sha512-BhrumqJSZCWFfLFUKl4CAUwR0Y0G2H5EfFVGKivVecEQbb+INAek1aa6c89evg2/OvetQYsJ+51QknskwqvLsA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-arm64-gnu": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-16.5.1.tgz", + "integrity": "sha512-x7MsSG0W+X43WVv7JhiSq2eKvH2suNKdlUHEG09Yt0vm3z0bhtym1UCMUg3IUAK7jy9hhLeDaFVFkC6zo+H/XQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-arm64-musl": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-16.5.1.tgz", + "integrity": "sha512-J+/v/mFjOm74I0PNtH5Ka+fDd+/dWbKhpcZ2R1/6b9agzZk+Ff/SrwJcSYFXXWKbPX+uQ4RcJoytT06Zs3s0ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-x64-gnu": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-16.5.1.tgz", + "integrity": "sha512-igooWJ5YxQ94Zft7IqgL+Lw0qHaY15Btw4gfK756g/YTYLZEt4tTvR1y6RnK/wdpE3sa68bFTLVBNCGTyiTiDQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-x64-musl": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-16.5.1.tgz", + "integrity": "sha512-zF/exnPqFYbrLAduGhTmZ7zNEyADid2bzNQiIjJkh8Y6NpDwrQIwVIyvIxqynsjMrIs51kBH+8TUjKjj2Jgf5A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-win32-arm64-msvc": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-16.5.1.tgz", + "integrity": "sha512-qtqiLS9Y9TYyAbbpq58kRoOroko4ZXg5oWVqIWFHoxc5bGPweQSJCROEqd1AOl2ZDC6BxfuVHfhDDop1kK05WA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-win32-x64-msvc": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-16.5.1.tgz", + "integrity": "sha512-kUJBLakK7iyA9WfsGGQBVennA4jwf5XIgm0lu35oMOphtZIluvzItMt0EYBmylEROpmpEIhHq0P6J9FA+WH0Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@parcel/watcher": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.0.4.tgz", @@ -5329,9 +5580,9 @@ } }, "node_modules/@polka/url": { - "version": "1.0.0-next.21", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", - "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", + "version": "1.0.0-next.23", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.23.tgz", + "integrity": "sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==", "dev": true }, "node_modules/@popperjs/core": { @@ -5396,13 +5647,13 @@ "integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==" }, "node_modules/@schematics/angular": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.2.1.tgz", - "integrity": "sha512-e3ckgvSv+OA+4xUBpOqVOvNM8FqY/yXaWqs/Ob0uQ/zPL1iVa/MCAoB25KqYQPnq21hEwE4zqIIQFKasKBIqMA==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.2.2.tgz", + "integrity": "sha512-OqPhpodkQx9pzSz7H2AGeEbf3ut6WOkJFP2YlX2JIGholfG/0FQMJmfTEyRoFXCBeVIDGt3sOmlfK7An0PS8uA==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.2.1", - "@angular-devkit/schematics": "16.2.1", + "@angular-devkit/core": "16.2.2", + "@angular-devkit/schematics": "16.2.2", "jsonc-parser": "3.2.0" }, "engines": { @@ -5411,14 +5662,41 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.2.tgz", + "integrity": "sha512-6H4FsvP3rLJaGiWpIhCFPS7ZeNoM4sSrnFtRhhecu6s7MidzE4aqzuGdzJpzLammw1KL+DuTlN0gpLtM1Bvcwg==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "picomatch": "2.3.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, "node_modules/@sentry-internal/tracing": { - "version": "7.66.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.66.0.tgz", - "integrity": "sha512-3vCgC2hC3T45pn53yTDVcRpHoJTBxelDPPZVsipAbZnoOVPkj7n6dNfDhj3I3kwWCBPahPkXmE+R4xViR8VqJg==", + "version": "7.69.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.69.0.tgz", + "integrity": "sha512-4BgeWZUj9MO6IgfO93C9ocP3+AdngqujF/+zB2rFdUe+y9S6koDyUC7jr9Knds/0Ta72N/0D6PwhgSCpHK8s0Q==", "dependencies": { - "@sentry/core": "7.66.0", - "@sentry/types": "7.66.0", - "@sentry/utils": "7.66.0", + "@sentry/core": "7.69.0", + "@sentry/types": "7.69.0", + "@sentry/utils": "7.69.0", "tslib": "^2.4.1 || ^1.9.3" }, "engines": { @@ -5426,13 +5704,13 @@ } }, "node_modules/@sentry/angular-ivy": { - "version": "7.66.0", - "resolved": "https://registry.npmjs.org/@sentry/angular-ivy/-/angular-ivy-7.66.0.tgz", - "integrity": "sha512-+/Z+c38J2PdZ7Okg8dC+tdu0tTdSz4Pngne4PPHkOsq4VP2pz4JUyMGDBqd7LaMXtcVurMLjxaweINFDC5Cb0w==", + "version": "7.69.0", + "resolved": "https://registry.npmjs.org/@sentry/angular-ivy/-/angular-ivy-7.69.0.tgz", + "integrity": "sha512-tXyTebex0O8ZUqS9RyTMNsVPM8z1Hr63W36Utg7jNd9Fi6XItQFWvOt4ME8/I89DIuCMBzhxCpwgPQqJolrhOQ==", "dependencies": { - "@sentry/browser": "7.66.0", - "@sentry/types": "7.66.0", - "@sentry/utils": "7.66.0", + "@sentry/browser": "7.69.0", + "@sentry/types": "7.69.0", + "@sentry/utils": "7.69.0", "tslib": "^2.4.1" }, "engines": { @@ -5446,15 +5724,15 @@ } }, "node_modules/@sentry/browser": { - "version": "7.66.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.66.0.tgz", - "integrity": "sha512-rW037rf8jkhyykG38+HUdwkRCKHJEMM5NkCqPIO5zuuxfLKukKdI2rbvgJ93s3/9UfsTuDFcKFL1u43mCn6sDw==", - "dependencies": { - "@sentry-internal/tracing": "7.66.0", - "@sentry/core": "7.66.0", - "@sentry/replay": "7.66.0", - "@sentry/types": "7.66.0", - "@sentry/utils": "7.66.0", + "version": "7.69.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.69.0.tgz", + "integrity": "sha512-5ls+zu2PrMhHCIIhclKQsWX5u6WH0Ez5/GgrCMZTtZ1d70ukGSRUvpZG9qGf5Cw1ezS1LY+1HCc3whf8x8lyPw==", + "dependencies": { + "@sentry-internal/tracing": "7.69.0", + "@sentry/core": "7.69.0", + "@sentry/replay": "7.69.0", + "@sentry/types": "7.69.0", + "@sentry/utils": "7.69.0", "tslib": "^2.4.1 || ^1.9.3" }, "engines": { @@ -5462,12 +5740,12 @@ } }, "node_modules/@sentry/core": { - "version": "7.66.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.66.0.tgz", - "integrity": "sha512-WMAEPN86NeCJ1IT48Lqiz4MS5gdDjBwP4M63XP4msZn9aujSf2Qb6My5uT87AJr9zBtgk8MyJsuHr35F0P3q1w==", + "version": "7.69.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.69.0.tgz", + "integrity": "sha512-V6jvK2lS8bhqZDMFUtvwe2XvNstFQf5A+2LMKCNBOV/NN6eSAAd6THwEpginabjet9dHsNRmMk7WNKvrUfQhZw==", "dependencies": { - "@sentry/types": "7.66.0", - "@sentry/utils": "7.66.0", + "@sentry/types": "7.69.0", + "@sentry/utils": "7.69.0", "tslib": "^2.4.1 || ^1.9.3" }, "engines": { @@ -5475,43 +5753,43 @@ } }, "node_modules/@sentry/replay": { - "version": "7.66.0", - "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.66.0.tgz", - "integrity": "sha512-5Y2SlVTOFTo3uIycv0mRneBakQtLgWkOnsJaC5LB0Ip0TqVKiMCbQ578vvXp+yvRj4LcS1gNd98xTTNojBoQNg==", + "version": "7.69.0", + "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.69.0.tgz", + "integrity": "sha512-oUqWyBPFUgShdVvgJtV65EQH9pVDmoYVQMOu59JI6FHVeL3ald7R5Mvz6GaNLXsirvvhp0yAkcAd2hc5Xi6hDw==", "dependencies": { - "@sentry/core": "7.66.0", - "@sentry/types": "7.66.0", - "@sentry/utils": "7.66.0" + "@sentry/core": "7.69.0", + "@sentry/types": "7.69.0", + "@sentry/utils": "7.69.0" }, "engines": { "node": ">=12" } }, "node_modules/@sentry/tracing": { - "version": "7.66.0", - "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.66.0.tgz", - "integrity": "sha512-9bnz2EcOwjeMZAuYJnrwcRrImu9c10p7A0iDB8b2HLcp7gpuCkJbJyGoC1xeKD7reVD0BPq3VIbeHSwCcQufoQ==", + "version": "7.69.0", + "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.69.0.tgz", + "integrity": "sha512-nhwJXyLU2KT6ci3YRUCkpFQH7RL9lpEuVDHqaJ9xLql766FJ7A7jKtRGSaefgRzJvvdKHUVboIjZnSvqIu8gWw==", "dependencies": { - "@sentry-internal/tracing": "7.66.0" + "@sentry-internal/tracing": "7.69.0" }, "engines": { "node": ">=8" } }, "node_modules/@sentry/types": { - "version": "7.66.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.66.0.tgz", - "integrity": "sha512-uUMSoSiar6JhuD8p7ON/Ddp4JYvrVd2RpwXJRPH1A4H4Bd4DVt1mKJy1OLG6HdeQv39XyhB1lPZckKJg4tATPw==", + "version": "7.69.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.69.0.tgz", + "integrity": "sha512-zPyCox0mzitzU6SIa1KIbNoJAInYDdUpdiA+PoUmMn2hFMH1llGU/cS7f4w/mAsssTlbtlBi72RMnWUCy578bw==", "engines": { "node": ">=8" } }, "node_modules/@sentry/utils": { - "version": "7.66.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.66.0.tgz", - "integrity": "sha512-9GYUVgXjK66uXXcLXVMXVzlptqMtq1eJENCuDeezQiEFrNA71KkLDg00wESp+LL+bl3wpVTBApArpbF6UEG5hQ==", + "version": "7.69.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.69.0.tgz", + "integrity": "sha512-4eBixe5Y+0EGVU95R4NxH3jkkjtkE4/CmSZD4In8SCkWGSauogePtq6hyiLsZuP1QHdpPb9Kt0+zYiBb2LouBA==", "dependencies": { - "@sentry/types": "7.66.0", + "@sentry/types": "7.69.0", "tslib": "^2.4.1 || ^1.9.3" }, "engines": { @@ -5827,9 +6105,9 @@ } }, "node_modules/@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "version": "3.4.36", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz", + "integrity": "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==", "dev": true, "peer": true, "dependencies": { @@ -5837,9 +6115,9 @@ } }, "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz", - "integrity": "sha512-4x5FkPpLipqwthjPsF7ZRbOv3uoLUFkTA9G9v583qi4pACvq0uTELrB8OLUzPWUI4IJIyvM85vzkV1nyiI2Lig==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.1.tgz", + "integrity": "sha512-iaQslNbARe8fctL5Lk+DsmgWOM83lM+7FzP0eQUJs1jd3kBE8NWqBTIT2S8SqQOJjxvt2eyIjpOuYeRXq2AdMw==", "dev": true, "peer": true, "dependencies": { @@ -5848,9 +6126,9 @@ } }, "node_modules/@types/crypto-js": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.1.1.tgz", - "integrity": "sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.1.2.tgz", + "integrity": "sha512-t33RNmTu5ufG/sorROIafiCVJMx3jz95bXUMoPAZcUD14fxMXnuTzqzXZoxpR0tNx2xpw11Dlmem9vGCsrSOfA==", "dev": true }, "node_modules/@types/d3-path": { @@ -6019,9 +6297,9 @@ "dev": true }, "node_modules/@types/lodash": { - "version": "4.14.197", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.197.tgz", - "integrity": "sha512-BMVOiWs0uNxHVlHBgzTIqJYmj+PgCo4euloGF+5m4okL3rEYzM2EEv78mw8zWSMM57dM7kVIgJ2QDvwHSoCI5g==", + "version": "4.14.198", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.198.tgz", + "integrity": "sha512-trNJ/vtMZYMLhfN45uLq4ShQSw0/S7xCTLLVM+WM1rmFpba/VS42jVUgaO3w/NOLiWR/09lnYk0yMaA/atdIsg==", "dev": true }, "node_modules/@types/lodash-es": { @@ -6041,9 +6319,9 @@ "peer": true }, "node_modules/@types/node": { - "version": "20.5.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", - "integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==", + "version": "20.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.0.tgz", + "integrity": "sha512-najjVq5KN2vsH2U/xyh2opaSEz6cZMR2SetLIlxlj08nOcmPOemJmUK2o4kUzfLqfrWE0PIrNeE16XhYDd3nqg==", "dev": true }, "node_modules/@types/papaparse": { @@ -6218,16 +6496,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.5.0.tgz", - "integrity": "sha512-2pktILyjvMaScU6iK3925uvGU87E+N9rh372uGZgiMYwafaw9SXq86U04XPq3UH6tzRvNgBsub6x2DacHc33lw==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.0.tgz", + "integrity": "sha512-gUqtknHm0TDs1LhY12K2NA3Rmlmp88jK9Tx8vGZMfHeNMLE3GH2e9TRub+y+SOjuYgtOmok+wt1AyDPZqxbNag==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.5.0", - "@typescript-eslint/type-utils": "6.5.0", - "@typescript-eslint/utils": "6.5.0", - "@typescript-eslint/visitor-keys": "6.5.0", + "@typescript-eslint/scope-manager": "6.7.0", + "@typescript-eslint/type-utils": "6.7.0", + "@typescript-eslint/utils": "6.7.0", + "@typescript-eslint/visitor-keys": "6.7.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -6253,13 +6531,13 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.5.0.tgz", - "integrity": "sha512-f7OcZOkRivtujIBQ4yrJNIuwyCQO1OjocVqntl9dgSIZAdKqicj3xFDqDOzHDlGCZX990LqhLQXWRnQvsapq8A==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.7.0.tgz", + "integrity": "sha512-f/QabJgDAlpSz3qduCyQT0Fw7hHpmhOzY/Rv6zO3yO+HVIdPfIWhrQoAyG+uZVtWAIS85zAyzgAFfyEr+MgBpg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.5.0", - "@typescript-eslint/utils": "6.5.0", + "@typescript-eslint/typescript-estree": "6.7.0", + "@typescript-eslint/utils": "6.7.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -6280,17 +6558,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.5.0.tgz", - "integrity": "sha512-9nqtjkNykFzeVtt9Pj6lyR9WEdd8npPhhIPM992FWVkZuS6tmxHfGVnlUcjpUP2hv8r4w35nT33mlxd+Be1ACQ==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.7.0.tgz", + "integrity": "sha512-MfCq3cM0vh2slSikQYqK2Gq52gvOhe57vD2RM3V4gQRZYX4rDPnKLu5p6cm89+LJiGlwEXU8hkYxhqqEC/V3qA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.5.0", - "@typescript-eslint/types": "6.5.0", - "@typescript-eslint/typescript-estree": "6.5.0", + "@typescript-eslint/scope-manager": "6.7.0", + "@typescript-eslint/types": "6.7.0", + "@typescript-eslint/typescript-estree": "6.7.0", "semver": "^7.5.4" }, "engines": { @@ -6305,15 +6583,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.5.0.tgz", - "integrity": "sha512-LMAVtR5GN8nY0G0BadkG0XIe4AcNMeyEy3DyhKGAh9k4pLSMBO7rF29JvDBpZGCmp5Pgz5RLHP6eCpSYZJQDuQ==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.0.tgz", + "integrity": "sha512-jZKYwqNpNm5kzPVP5z1JXAuxjtl2uG+5NpaMocFPTNC2EdYIgbXIPImObOkhbONxtFTTdoZstLZefbaK+wXZng==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.5.0", - "@typescript-eslint/types": "6.5.0", - "@typescript-eslint/typescript-estree": "6.5.0", - "@typescript-eslint/visitor-keys": "6.5.0", + "@typescript-eslint/scope-manager": "6.7.0", + "@typescript-eslint/types": "6.7.0", + "@typescript-eslint/typescript-estree": "6.7.0", + "@typescript-eslint/visitor-keys": "6.7.0", "debug": "^4.3.4" }, "engines": { @@ -6333,13 +6611,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.5.0.tgz", - "integrity": "sha512-A8hZ7OlxURricpycp5kdPTH3XnjG85UpJS6Fn4VzeoH4T388gQJ/PGP4ole5NfKt4WDVhmLaQ/dBLNDC4Xl/Kw==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.7.0.tgz", + "integrity": "sha512-lAT1Uau20lQyjoLUQ5FUMSX/dS07qux9rYd5FGzKz/Kf8W8ccuvMyldb8hadHdK/qOI7aikvQWqulnEq2nCEYA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.5.0", - "@typescript-eslint/visitor-keys": "6.5.0" + "@typescript-eslint/types": "6.7.0", + "@typescript-eslint/visitor-keys": "6.7.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -6434,9 +6712,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.5.0.tgz", - "integrity": "sha512-eqLLOEF5/lU8jW3Bw+8auf4lZSbbljHR2saKnYqON12G/WsJrGeeDHWuQePoEf9ro22+JkbPfWQwKEC5WwLQ3w==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.7.0.tgz", + "integrity": "sha512-ihPfvOp7pOcN/ysoj0RpBPOx3HQTJTrIN8UZK+WFd3/iDeFHHqeyYxa4hQk4rMhsz9H9mXpR61IzwlBVGXtl9Q==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -6447,13 +6725,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.5.0.tgz", - "integrity": "sha512-q0rGwSe9e5Kk/XzliB9h2LBc9tmXX25G0833r7kffbl5437FPWb2tbpIV9wAATebC/018pGa9fwPDuvGN+LxWQ==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.0.tgz", + "integrity": "sha512-dPvkXj3n6e9yd/0LfojNU8VMUGHWiLuBZvbM6V6QYD+2qxqInE7J+J/ieY2iGwR9ivf/R/haWGkIj04WVUeiSQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.5.0", - "@typescript-eslint/visitor-keys": "6.5.0", + "@typescript-eslint/types": "6.7.0", + "@typescript-eslint/visitor-keys": "6.7.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -6596,12 +6874,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.5.0.tgz", - "integrity": "sha512-yCB/2wkbv3hPsh02ZS8dFQnij9VVQXJMN/gbQsaaY+zxALkZnxa/wagvLEFsAWMPv7d7lxQmNsIzGU1w/T/WyA==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.0.tgz", + "integrity": "sha512-/C1RVgKFDmGMcVGeD8HjKv2bd72oI1KxQDeY8uc66gw9R0OK0eMq48cA+jv9/2Ag6cdrsUGySm1yzYmfz0hxwQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.5.0", + "@typescript-eslint/types": "6.7.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -6964,9 +7242,9 @@ } }, "node_modules/ace-builds": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.24.1.tgz", - "integrity": "sha512-TLcxMxiTRX5Eq9bBVSd/bTJlanCBULiv/IULLohJDDaCAfcpZKJBVSd4OWfN/j2c2jCLc+jhpNWGELiJZw3wPw==" + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.25.1.tgz", + "integrity": "sha512-pB4N8wvl+tUEwD12BovBUpd6B+IpASOShd8WlufwFnXCfBQk/4nwmpN5vZSsvd6v5G7YaP9/PPdQK4cq2ZRzng==" }, "node_modules/acorn": { "version": "8.10.0", @@ -8067,9 +8345,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001525", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001525.tgz", - "integrity": "sha512-/3z+wB4icFt3r0USMwxujAqRvaD/B7rvGTsKhbhSQErVrJvkZCLhgNLJxU8MevahQVH6hCU9FsHdNUFbiwmE7Q==", + "version": "1.0.30001529", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001529.tgz", + "integrity": "sha512-n2pUQYGAkrLG4QYj2desAh+NqsJpHbNmVZz87imptDdxLAtjxary7Df/psdfyDGmskJK/9Dt9cPnx5RZ3CU4Og==", "funding": [ { "type": "opencollective", @@ -8626,9 +8904,9 @@ } }, "node_modules/core-js": { - "version": "3.32.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.32.1.tgz", - "integrity": "sha512-lqufgNn9NLnESg5mQeYsxQP5w7wrViSj0jr/kv6ECQiByzQkrn1MKvV0L3acttpDqfQrHLwr2KCMgX5b8X+lyQ==", + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.32.2.tgz", + "integrity": "sha512-pxXSw1mYZPDGvTQqEc5vgIb83jGQKFGYWY76z4a7weZXUolw3G+OvpZqSRcfYOoOVUQJYEPsWeQK8pKEnUtWxQ==", "hasInstallScript": true, "funding": { "type": "opencollective", @@ -8636,9 +8914,9 @@ } }, "node_modules/core-js-compat": { - "version": "3.32.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.1.tgz", - "integrity": "sha512-GSvKDv4wE0bPnQtjklV101juQ85g6H3rm5PDP20mqlS5j0kXF3pP97YvAu5hl+uFHqMictp3b2VxOHljWMAtuA==", + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.2.tgz", + "integrity": "sha512-+GjlguTDINOijtVRUxrQOv3kfu9rl+qPNdX2LTbJ/ZyVTuxK+ksVSAGX1nHstu4hrv1En/uPTtWgq2gI5wt4AQ==", "dev": true, "peer": true, "dependencies": { @@ -8655,15 +8933,15 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, "node_modules/cosmiconfig": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.2.0.tgz", - "integrity": "sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==", + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.4.tgz", + "integrity": "sha512-SF+2P8+o/PTV05rgsAjDzL4OFdVXAulSfC/L19VaeVT7+tpOOSscCt2QLxDZ+CLxF2WOiq6y1K5asvs8qUJT/Q==", "dev": true, "peer": true, "dependencies": { - "import-fresh": "^3.2.1", + "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", - "parse-json": "^5.0.0", + "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "engines": { @@ -8671,6 +8949,14 @@ }, "funding": { "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/cosmiconfig/node_modules/argparse": { @@ -9693,9 +9979,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.506", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.506.tgz", - "integrity": "sha512-xxGct4GPAKSRlrLBtJxJFYy74W11zX6PO9GyHgl/U+2s3Dp0ZEwAklDfNHXOWcvH7zWMpsmgbR0ggEuaYAVvHA==" + "version": "1.4.513", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.513.tgz", + "integrity": "sha512-cOB0xcInjm+E5qIssHeXJ29BaUyWpMyFKT5RB3bsLENDheCja0wMkHJyiPl0NBE/VzDI7JDuNEQWhe6RitEUcw==" }, "node_modules/emittery": { "version": "0.13.1", @@ -9973,16 +10259,16 @@ } }, "node_modules/eslint": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.48.0.tgz", - "integrity": "sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", + "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.48.0", - "@humanwhocodes/config-array": "^0.11.10", + "@eslint/js": "8.49.0", + "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.12.4", @@ -10039,18 +10325,43 @@ } }, "node_modules/eslint-plugin-deprecation": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-deprecation/-/eslint-plugin-deprecation-1.5.0.tgz", - "integrity": "sha512-mRcssI/tLROueBQ6yf4LnnGTijbMsTCPIpbRbPj5R5wGYVCpk1zDmAS0SEkgcUDXOPc22qMNFR24Qw7vSPrlTA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-deprecation/-/eslint-plugin-deprecation-2.0.0.tgz", + "integrity": "sha512-OAm9Ohzbj11/ZFyICyR5N6LbOIvQMp7ZU2zI7Ej0jIc8kiGUERXPNMfw2QqqHD1ZHtjMub3yPZILovYEYucgoQ==", "dev": true, "dependencies": { - "@typescript-eslint/utils": "^5.57.0", + "@typescript-eslint/utils": "^6.0.0", "tslib": "^2.3.1", "tsutils": "^3.21.0" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0", - "typescript": "^3.7.5 || ^4.0.0 || ^5.0.0" + "eslint": "^7.0.0 || ^8.0.0", + "typescript": "^4.2.4 || ^5.0.0" + } + }, + "node_modules/eslint-plugin-deprecation/node_modules/@typescript-eslint/utils": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.7.0.tgz", + "integrity": "sha512-MfCq3cM0vh2slSikQYqK2Gq52gvOhe57vD2RM3V4gQRZYX4rDPnKLu5p6cm89+LJiGlwEXU8hkYxhqqEC/V3qA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.7.0", + "@typescript-eslint/types": "6.7.0", + "@typescript-eslint/typescript-estree": "6.7.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" } }, "node_modules/eslint-plugin-jest": { @@ -10960,9 +11271,9 @@ } }, "node_modules/fraction.js": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.4.tgz", - "integrity": "sha512-pwiTgt0Q7t+GHZA4yaLjObx4vXmmdcS0iSJ19o8d/goUGgItX9UZWKWNnLHehxviD8wU2IWRsnR8cD5+yOJP2Q==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.6.tgz", + "integrity": "sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==", "dev": true, "peer": true, "engines": { @@ -12595,9 +12906,9 @@ "integrity": "sha512-4dG1D1x/7g8PwHS9aK6QV5V94+ZvyP4+d19qDv43EzImmrndysIl4prmJ1hWWIGCqrZHyaHBm6BSEWHOLnpoNw==" }, "node_modules/jackspeak": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.1.tgz", - "integrity": "sha512-4iSY3Bh1Htv+kLhiiZunUhQ+OYXIn0ze3ulq8JeWrFKmhPAJSySV2+kdtRh2pGcCeF0s6oR8Oc+pYZynJj4t8A==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.3.tgz", + "integrity": "sha512-R2bUw+kVZFS/h1AZqBKrSgDmdmjApzgY0AlCPumopFiAlbUxE2gf+SCuBzQ0cP5hHmUmFYF5yw55T97Th5Kstg==", "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -13523,9 +13834,9 @@ } }, "node_modules/jest-environment-jsdom/node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.1.tgz", + "integrity": "sha512-4OOseMUq8AzRBI/7SLMUwO+FEDnguetSk7KMb1sHwvF2w2Wv5Hoj0nlifx8vtGsftE/jWHojPy8sMMzYLJ2G/A==", "dev": true, "engines": { "node": ">=10.0.0" @@ -14726,9 +15037,9 @@ } }, "node_modules/jiti": { - "version": "1.19.3", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.19.3.tgz", - "integrity": "sha512-5eEbBDQT/jF1xg6l36P+mWGGoH9Spuy0PCdSr2dtWRDGC6ph/w9ZCL4lmESW8f8F7MwT3XKescfP0wnZWAKL9w==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.20.0.tgz", + "integrity": "sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==", "dev": true, "peer": true, "bin": { @@ -17923,9 +18234,9 @@ } }, "node_modules/pure-rand": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.2.tgz", - "integrity": "sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.3.tgz", + "integrity": "sha512-KddyFewCsO0j3+np81IQ+SweXLDnDQTs5s67BOnrYmYe/yNmUhttQyGsYzy8yUnoljGAQ9sl38YB4vH8ur7Y+w==", "dev": true, "funding": [ { @@ -18562,9 +18873,9 @@ } }, "node_modules/rollup": { - "version": "3.28.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.28.1.tgz", - "integrity": "sha512-R9OMQmIHJm9znrU3m3cpE8uhN0fGdXiawME7aZIpQqvpS/85+Vt1Hq1/yVIcYfOmaQiHjvXkQAoJukvLpau6Yw==", + "version": "3.29.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.0.tgz", + "integrity": "sha512-nszM8DINnx1vSS+TpbWKMkxem0CDWk3cSit/WWCBVs9/JZ1I/XLwOsiUglYuYReaeWWSsW9kge5zE5NZtf/a4w==", "dev": true, "peer": true, "bin": { @@ -18649,9 +18960,9 @@ "integrity": "sha512-LRneZZRXNgjzwG4bDQdOTSbze3fHm1EAKN/8bePxnlEZiBmkYEDggaHbuvHI9/hoqHbGfsEA7tWS9GhYHZBBsw==" }, "node_modules/sass": { - "version": "1.66.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.66.1.tgz", - "integrity": "sha512-50c+zTsZOJVgFfTgwwEzkjA3/QACgdNsKueWPyAR0mRINIvLAStVQBbPg14iuqEQ74NPDbXzJARJ/O4SI1zftA==", + "version": "1.67.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.67.0.tgz", + "integrity": "sha512-SVrO9ZeX/QQyEGtuZYCVxoeAL5vGlYjJ9p4i4HFuekWl8y/LtJ7tJc10Z+ck1c8xOuoBm2MYzcLfTAffD0pl/A==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -19708,9 +20019,9 @@ } }, "node_modules/tar": { - "version": "6.1.15", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz", - "integrity": "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", "dev": true, "dependencies": { "chownr": "^2.0.0", @@ -20111,9 +20422,9 @@ } }, "node_modules/ts-api-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.2.tgz", - "integrity": "sha512-Cbu4nIqnEdd+THNEsBdkolnOXhg0I8XteoHaEKgvsxpsbWda4IsUut2c187HxywQCvveojow0Dgw/amxtSKVkQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", "dev": true, "engines": { "node": ">=16.13.0" @@ -20490,9 +20801,13 @@ } }, "node_modules/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { "uuid": "dist/bin/uuid" } @@ -20954,9 +21269,9 @@ } }, "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.1.tgz", + "integrity": "sha512-4OOseMUq8AzRBI/7SLMUwO+FEDnguetSk7KMb1sHwvF2w2Wv5Hoj0nlifx8vtGsftE/jWHojPy8sMMzYLJ2G/A==", "dev": true, "peer": true, "engines": { diff --git a/package.json b/package.json index 33c99708233e..a21c26412c31 100644 --- a/package.json +++ b/package.json @@ -13,18 +13,18 @@ "node_modules" ], "dependencies": { - "@angular/animations": "16.2.3", - "@angular/cdk": "16.2.2", - "@angular/common": "16.2.3", - "@angular/compiler": "16.2.3", - "@angular/core": "16.2.3", - "@angular/forms": "16.2.3", - "@angular/localize": "16.2.3", - "@angular/material": "16.2.2", - "@angular/platform-browser": "16.2.3", - "@angular/platform-browser-dynamic": "16.2.3", - "@angular/router": "16.2.3", - "@angular/service-worker": "16.2.3", + "@angular/animations": "16.2.5", + "@angular/cdk": "16.2.4", + "@angular/common": "16.2.5", + "@angular/compiler": "16.2.5", + "@angular/core": "16.2.5", + "@angular/forms": "16.2.5", + "@angular/localize": "16.2.5", + "@angular/material": "16.2.4", + "@angular/platform-browser": "16.2.5", + "@angular/platform-browser-dynamic": "16.2.5", + "@angular/router": "16.2.5", + "@angular/service-worker": "16.2.5", "@ctrl/ngx-emoji-mart": "9.2.0", "@danielmoncada/angular-datetime-picker": "16.0.1", "@fingerprintjs/fingerprintjs": "4.0.1", @@ -37,16 +37,16 @@ "@ng-bootstrap/ng-bootstrap": "15.1.1", "@ngx-translate/core": "15.0.0", "@ngx-translate/http-loader": "8.0.0", - "@sentry/angular-ivy": "7.66.0", - "@sentry/tracing": "7.66.0", - "@sentry/types": "7.66.0", + "@sentry/angular-ivy": "7.69.0", + "@sentry/tracing": "7.69.0", + "@sentry/types": "7.69.0", "@swimlane/ngx-charts": "20.4.1", "@swimlane/ngx-graph": "8.2.2", - "ace-builds": "1.24.1", + "ace-builds": "1.25.1", "bootstrap": "5.3.1", "brace": "0.11.1", "compare-versions": "6.1.0", - "core-js": "3.32.1", + "core-js": "3.32.2", "crypto-js": "4.1.1", "dayjs": "1.11.9", "diff-match-patch-typescript": "1.0.8", @@ -73,7 +73,7 @@ "split.js": "1.6.5", "ts-cacheable": "1.0.9", "tslib": "2.6.2", - "uuid": "9.0.0", + "uuid": "9.0.1", "webstomp-client": "1.2.6", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.0/xlsx-0.20.0.tgz", "zone.js": "0.13.0" @@ -95,30 +95,30 @@ }, "devDependencies": { "@angular-builders/jest": "16.0.1", - "@angular-eslint/builder": "16.1.1", - "@angular-eslint/eslint-plugin": "16.1.1", - "@angular-eslint/eslint-plugin-template": "16.1.1", - "@angular-eslint/schematics": "16.1.1", - "@angular-eslint/template-parser": "16.1.1", - "@angular/cli": "16.2.1", - "@angular/compiler-cli": "16.2.3", - "@angular/language-service": "16.2.3", - "@types/crypto-js": "4.1.1", + "@angular-eslint/builder": "16.1.2", + "@angular-eslint/eslint-plugin": "16.1.2", + "@angular-eslint/eslint-plugin-template": "16.1.2", + "@angular-eslint/schematics": "16.1.2", + "@angular-eslint/template-parser": "16.1.2", + "@angular/cli": "16.2.2", + "@angular/compiler-cli": "16.2.5", + "@angular/language-service": "16.2.5", + "@types/crypto-js": "4.1.2", "@types/d3-shape": "3.1.2", "@types/dompurify": "3.0.2", "@types/jest": "29.5.4", "@types/lodash-es": "4.17.9", - "@types/node": "20.5.7", + "@types/node": "20.6.0", "@types/papaparse": "5.3.8", "@types/showdown": "2.0.1", "@types/smoothscroll-polyfill": "0.3.1", "@types/sockjs-client": "1.5.1", "@types/uuid": "9.0.3", - "@typescript-eslint/eslint-plugin": "6.5.0", - "@typescript-eslint/parser": "6.5.0", - "eslint": "8.48.0", + "@typescript-eslint/eslint-plugin": "6.7.0", + "@typescript-eslint/parser": "6.7.0", + "eslint": "8.49.0", "eslint-config-prettier": "9.0.0", - "eslint-plugin-deprecation": "1.5.0", + "eslint-plugin-deprecation": "2.0.0", "eslint-plugin-jest": "27.2.3", "eslint-plugin-jest-extended": "2.0.0", "eslint-plugin-prettier": "5.0.0", @@ -134,7 +134,7 @@ "lint-staged": "14.0.1", "ng-mocks": "14.11.0", "prettier": "3.0.3", - "sass": "1.66.1", + "sass": "1.67.0", "ts-jest": "29.1.1", "typescript": "5.1.6", "weak-napi": "2.0.2", diff --git a/src/main/java/de/tum/in/www1/artemis/domain/enumeration/DataExportState.java b/src/main/java/de/tum/in/www1/artemis/domain/enumeration/DataExportState.java index 5f4331897e67..1789c1d2ce5d 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/enumeration/DataExportState.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/enumeration/DataExportState.java @@ -8,10 +8,28 @@ public enum DataExportState { REQUESTED, IN_CREATION, EMAIL_SENT, DOWNLOADED, DOWNLOADED_DELETED, DELETED, FAILED; + /** + * Checks if the data export can be downloaded. + *

+ * The data export can be downloaded if its state is either EMAIL_SENT or DOWNLOADED. + * The state is EMAIL_SENT if the data export has been created and the user has been notified via email. + * The state is DOWNLOADED if the user has downloaded the data export at least once. + * + * @return true if the data export can be downloaded, false otherwise + */ public boolean isDownloadable() { return this == DOWNLOADED || this == EMAIL_SENT; } + /** + * Checks if the data export has been downloaded. + *

+ * The data export has been downloaded if its state is either DOWNLOADED or DOWNLOADED_DELETED. + * The state is DOWNLOADED if the user has downloaded the data export at least once, but it has not been deleted yet. + * The state is DOWNLOADED_DELETED if the user has downloaded the data export at least once, and it has been deleted. + * + * @return true if the data export has been downloaded, false otherwise + */ public boolean hasBeenDownloaded() { return this == DOWNLOADED || this == DOWNLOADED_DELETED; } diff --git a/src/main/java/de/tum/in/www1/artemis/repository/DataExportRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/DataExportRepository.java index 9315ccd9d78a..4344193419f0 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/DataExportRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/DataExportRepository.java @@ -1,5 +1,6 @@ package de.tum.in.www1.artemis.repository; +import java.util.List; import java.util.Set; import org.springframework.data.jpa.repository.JpaRepository; @@ -55,12 +56,23 @@ default DataExport findByIdElseThrow(long dataExportId) { """) Set findAllToBeDeleted(); + /** + * Find all data exports for the given user ordered by their request date descending. + * We use this sorting because this allows us to always get the latest data export without a doing any other calculations. + *

+ * This is relevant if more than one data export exists that can be downloaded. + * This can happen if the user had requested a data export that was created and the admin requested another data export for the same user that has been created. + * + * @param userId the id of the user to find the data exports for + * @return a list of data exports for the given user ordered by their request date descending + */ @Query(""" SELECT dataExport FROM DataExport dataExport WHERE dataExport.user.id = :userId + ORDER BY dataExport.createdDate DESC """) - Set findAllDataExportsByUserId(long userId); + List findAllDataExportsByUserIdOrderByRequestDateDesc(long userId); @Query(""" SELECT dataExport diff --git a/src/main/java/de/tum/in/www1/artemis/service/FileService.java b/src/main/java/de/tum/in/www1/artemis/service/FileService.java index 84877534deb6..662e15d40ebf 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/FileService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/FileService.java @@ -26,7 +26,6 @@ import org.apache.commons.io.filefilter.FileFilterUtils; import org.apache.commons.io.filefilter.IOFileFilter; import org.apache.commons.lang3.math.NumberUtils; -import org.apache.pdfbox.io.MemoryUsageSetting; import org.apache.pdfbox.multipdf.PDFMergerUtility; import org.apache.pdfbox.pdmodel.PDDocumentInformation; import org.apache.tomcat.util.http.fileupload.IOUtils; @@ -1203,7 +1202,7 @@ public Optional mergePdfFiles(List paths, String mergedPdfFileNa for (String path : paths) { File file = new File(path); if (file.exists()) { - pdfMerger.addSource(new File(path)); + pdfMerger.addSource(file); } } @@ -1212,7 +1211,7 @@ public Optional mergePdfFiles(List paths, String mergedPdfFileNa pdfMerger.setDestinationDocumentInformation(pdDocumentInformation); pdfMerger.setDestinationStream(outputStream); - pdfMerger.mergeDocuments(MemoryUsageSetting.setupTempFileOnly()); + pdfMerger.mergeDocuments(null); } catch (IOException e) { diff --git a/src/main/java/de/tum/in/www1/artemis/service/LectureUnitProcessingService.java b/src/main/java/de/tum/in/www1/artemis/service/LectureUnitProcessingService.java index e73e33e39546..fc04fa5bf323 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LectureUnitProcessingService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LectureUnitProcessingService.java @@ -6,6 +6,7 @@ import javax.validation.constraints.NotNull; +import org.apache.pdfbox.Loader; import org.apache.pdfbox.multipdf.Splitter; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocumentInformation; @@ -55,18 +56,22 @@ public LectureUnitProcessingService(SlideSplitterService slideSplitterService, F */ public List splitAndSaveUnits(LectureUnitInformationDTO lectureUnitInformationDTO, MultipartFile file, Lecture lecture) throws IOException { - try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); PDDocument document = PDDocument.load(file.getBytes())) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); PDDocument document = Loader.loadPDF(file.getBytes())) { List units = new ArrayList<>(); Splitter pdfSplitter = new Splitter(); for (LectureUnitSplitDTO lectureUnit : lectureUnitInformationDTO.units()) { + // make sure output stream doesn't contain old data + outputStream.reset(); + AttachmentUnit attachmentUnit = new AttachmentUnit(); Attachment attachment = new Attachment(); PDDocumentInformation pdDocumentInformation = new PDDocumentInformation(); pdfSplitter.setStartPage(lectureUnit.startPage()); pdfSplitter.setEndPage(lectureUnit.endPage()); - pdfSplitter.setSplitAtPage(lectureUnit.endPage()); + // split only based on start and end page + pdfSplitter.setSplitAtPage(document.getNumberOfPages()); List documentUnits = pdfSplitter.split(document); pdDocumentInformation.setTitle(lectureUnit.unitName()); @@ -165,7 +170,7 @@ public LectureUnitInformationDTO getSplitUnitData(MultipartFile file) { * @return The prepared map */ private Outline separateIntoUnits(MultipartFile file) throws IOException { - try (PDDocument document = PDDocument.load(file.getBytes())) { + try (PDDocument document = Loader.loadPDF(file.getBytes())) { Map outlineMap = new HashMap<>(); Splitter pdfSplitter = new Splitter(); PDFTextStripper pdfStripper = new PDFTextStripper(); diff --git a/src/main/java/de/tum/in/www1/artemis/service/ResultService.java b/src/main/java/de/tum/in/www1/artemis/service/ResultService.java index 46aeaa058d6f..cd624ad5be21 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ResultService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ResultService.java @@ -217,7 +217,7 @@ public List filterFeedbackForClient(Result result) { * @param result a result of this participation */ public void filterSensitiveInformationIfNecessary(final Participation participation, final Result result) { - this.filterSensitiveInformationIfNecessary(participation, List.of(result)); + this.filterSensitiveInformationIfNecessary(participation, List.of(result), Optional.empty()); } /** @@ -225,29 +225,41 @@ public void filterSensitiveInformationIfNecessary(final Participation participat * * @param participation the results belong to. * @param results collection of results of this participation + * @param user the user for which the information should be filtered if it is an empty optional, the currently logged-in user is used */ - public void filterSensitiveInformationIfNecessary(final Participation participation, final Collection results) { + public void filterSensitiveInformationIfNecessary(final Participation participation, final Collection results, Optional user) { results.forEach(Result::filterSensitiveInformation); - if (!authCheckService.isAtLeastTeachingAssistantForExercise(participation.getExercise())) { - // The test cases marked as after_due_date should only be shown after all - // students can no longer submit so that no unfair advantage is possible. - // - // For course exercises, this applies only to automatic results. For manual ones the instructors - // are responsible to set an appropriate assessment due date. - // - // For exams, we filter sensitive results until the results are published. - // For test exam exercises, this is the case when the student submitted the test exam. - - Exercise exercise = participation.getExercise(); - if (exercise.isExamExercise()) { - filterSensitiveFeedbacksInExamExercise(participation, results, exercise); + if (user.isPresent()) { + if (!authCheckService.isAtLeastTeachingAssistantForExercise(participation.getExercise(), user.get())) { + filterInformation(participation, results); } - else { - filterSensitiveFeedbackInCourseExercise(participation, results, exercise); + } + else { + if (!authCheckService.isAtLeastTeachingAssistantForExercise(participation.getExercise())) { + filterInformation(participation, results); } } } + private void filterInformation(Participation participation, Collection results) { + // The test cases marked as after_due_date should only be shown after all + // students can no longer submit so that no unfair advantage is possible. + // + // For course exercises, this applies only to automatic results. For manual ones the instructors + // are responsible to set an appropriate assessment due date. + // + // For exams, we filter sensitive results until the results are published. + // For test exam exercises, this is the case when the student submitted the test exam. + + Exercise exercise = participation.getExercise(); + if (exercise.isExamExercise()) { + filterSensitiveFeedbacksInExamExercise(participation, results, exercise); + } + else { + filterSensitiveFeedbackInCourseExercise(participation, results, exercise); + } + } + private void filterSensitiveFeedbackInCourseExercise(Participation participation, Collection results, Exercise exercise) { boolean beforeLatestDueDate = exerciseDateService.isBeforeLatestDueDate(exercise); boolean participationBeforeDueDate = exerciseDateService.isBeforeDueDate(participation); diff --git a/src/main/java/de/tum/in/www1/artemis/service/SlideSplitterService.java b/src/main/java/de/tum/in/www1/artemis/service/SlideSplitterService.java index 87558232de2f..623f49fe8933 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/SlideSplitterService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/SlideSplitterService.java @@ -8,6 +8,7 @@ import javax.imageio.ImageIO; import org.apache.commons.io.FilenameUtils; +import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.rendering.ImageType; import org.apache.pdfbox.rendering.PDFRenderer; @@ -48,7 +49,7 @@ public SlideSplitterService(FileService fileService, SlideRepository slideReposi public void splitAttachmentUnitIntoSingleSlides(AttachmentUnit attachmentUnit) { String attachmentPath = fileService.actualPathForPublicPath(attachmentUnit.getAttachment().getLink()); File file = new File(attachmentPath); - try (PDDocument document = PDDocument.load(file)) { + try (PDDocument document = Loader.loadPDF(file)) { String pdfFilename = file.getName(); splitAttachmentUnitIntoSingleSlides(document, attachmentUnit, pdfFilename); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/dataexport/DataExportCommunicationDataService.java b/src/main/java/de/tum/in/www1/artemis/service/dataexport/DataExportCommunicationDataService.java index 3b05483bd5e2..639cd17f67ac 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/dataexport/DataExportCommunicationDataService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/dataexport/DataExportCommunicationDataService.java @@ -25,6 +25,8 @@ /** * A service to create the communication data export for users + * This includes messages (posts), thread replies (answer posts) and reactions to posts and answer posts + * All communication data is exported per course and stored in a CSV file. */ @Service public class DataExportCommunicationDataService { @@ -67,6 +69,12 @@ public void createCommunicationDataExport(long userId, Path workingDirectory) th createCommunicationDataExportIfReactionsToAnswerPostsExist(workingDirectory, reactionsToAnswerPostsPerCourse); } + /** + * Creates the communication data export for a course if only reactions to answer posts exist + * + * @param workingDirectory the directory where the export is stored + * @param reactionsToAnswerPostsPerCourse the reactions to answer posts grouped by course + */ private void createCommunicationDataExportIfReactionsToAnswerPostsExist(Path workingDirectory, Map> reactionsToAnswerPostsPerCourse) throws IOException { // it can happen that only answer post reactions exist in a course but neither posts, nor answer posts nor reactions to posts for (var entry : reactionsToAnswerPostsPerCourse.entrySet()) { @@ -78,6 +86,13 @@ private void createCommunicationDataExportIfReactionsToAnswerPostsExist(Path wor } } + /** + * Creates the communication data export for a course if only reactions to posts (and potentially to answer posts) exist + * + * @param workingDirectory the directory where the export is stored + * @param reactionsToPostsPerCourse the reactions to posts grouped by course + * @param reactionsToAnswerPostsPerCourse the reactions to answer posts grouped by course + */ private void createCommunicationDataExportIfReactionsToPostsExist(Path workingDirectory, Map> reactionsToPostsPerCourse, Map> reactionsToAnswerPostsPerCourse) throws IOException { // it can happen that only reactions exist in a course but no post or answer post @@ -91,6 +106,14 @@ private void createCommunicationDataExportIfReactionsToPostsExist(Path workingDi } } + /** + * Creates the communication data export for a course if only answer posts (and potentially reactions to post and answer posts) exist + * + * @param workingDirectory the directory where the export is stored + * @param answerPostsPerCourse the answer posts grouped by course + * @param reactionsToPostsPerCourse the reactions to posts grouped by course + * @param reactionsToAnswerPostsPerCourse the reactions to answer posts grouped by course + */ private void createCommunicationDataExportIfAnswerPostsExist(Path workingDirectory, Map> answerPostsPerCourse, Map> reactionsToPostsPerCourse, Map> reactionsToAnswerPostsPerCourse) throws IOException { // it can happen that an answer post and reactions exist in a course but no post @@ -105,6 +128,15 @@ private void createCommunicationDataExportIfAnswerPostsExist(Path workingDirecto } } + /** + * Creates the communication data export for a course if posts exist + * + * @param workingDirectory the directory where the export is stored + * @param postsPerCourse the posts grouped by course + * @param answerPostsPerCourse the answer posts grouped by course + * @param reactionsToPostsPerCourse the reactions to posts grouped by course + * @param reactionsToAnswerPostsPerCourse the reactions to answer posts grouped by course + */ private void createCommunicationDataExportIfPostsExist(Path workingDirectory, Map> postsPerCourse, Map> answerPostsPerCourse, Map> reactionsToPostsPerCourse, Map> reactionsToAnswerPostsPerCourse) throws IOException { // this covers all cases where at least one post in a course exists @@ -121,6 +153,15 @@ private void createCommunicationDataExportIfPostsExist(Path workingDirectory, Ma } } + /** + * Creates the actual CSV file containing the communication data for a course + * + * @param courseDir the directory where the CSV file is stored + * @param postsInCourse the posts in the course + * @param answerPostsInCourse the answer posts in the course + * @param postReactionsInCourse the reactions to posts in the course + * @param answerPostReactionsInCourse the reactions to answer posts in the course + */ private void createCommunicationDataCsvFile(Path courseDir, List postsInCourse, List answerPostsInCourse, List postReactionsInCourse, List answerPostReactionsInCourse) throws IOException { diff --git a/src/main/java/de/tum/in/www1/artemis/service/dataexport/DataExportCreationService.java b/src/main/java/de/tum/in/www1/artemis/service/dataexport/DataExportCreationService.java index 6385ad16e8c5..bbc02bf87f73 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/dataexport/DataExportCreationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/dataexport/DataExportCreationService.java @@ -30,6 +30,8 @@ /** * A service to create data exports for users + * This service is responsible for creating the data export, delegating most tasks to the {@link DataExportExerciseCreationService} and {@link DataExportExamCreationService} + * and notifying the user about the creation. */ @Service public class DataExportCreationService { @@ -88,7 +90,7 @@ private DataExport createDataExportWithContent(DataExport dataExport) throws IOE var userId = dataExport.getUser().getId(); var user = dataExport.getUser(); var workingDirectory = prepareDataExport(dataExport); - dataExportExerciseCreationService.createExercisesExport(workingDirectory, userId); + dataExportExerciseCreationService.createExercisesExport(workingDirectory, user); dataExportExamCreationService.createExportForExams(userId, workingDirectory); dataExportCommunicationDataService.createCommunicationDataExport(userId, workingDirectory); addGeneralUserInformation(user, workingDirectory); @@ -97,6 +99,17 @@ private DataExport createDataExportWithContent(DataExport dataExport) throws IOE return finishDataExportCreation(dataExport, dataExportPath); } + /** + * Adds a markdown file with the title README.md to the data export. + *

+ * This file contains information Art. 15 GDPR requires us to provide to the user. + * The file is retrieved from the resources folder. + * The file is added to the root of the data export. + * + * @param workingDirectory the directory in which the data export is created + * @throws IOException if the file could not be copied + * @throws URISyntaxException if the resource file path is invalid + */ private void addReadmeFile(Path workingDirectory) throws IOException, URISyntaxException { var readmeInDataExportPath = workingDirectory.resolve("README.md"); var readmeTemplatePath = Path.of("templates", "dataexport", "README.md"); @@ -105,6 +118,8 @@ private void addReadmeFile(Path workingDirectory) throws IOException, URISyntaxE /** * Creates the data export for the given user. + *

+ * This includes creation of the export and notifying the user about the creation. * * @param dataExport the data export to be created * @return true if the export was successful, false otherwise @@ -129,6 +144,15 @@ public boolean createDataExport(DataExport dataExport) { return true; } + /** + * Handles the case of a failed data export creation. + *

+ * This includes setting the state of the data export to failed, notifying the user about the failure and sending an email to the admin with the exception why the export + * failed. + * + * @param dataExport the data export that failed to be created + * @param exception the exception that occurred during the creation + */ private void handleCreationFailure(DataExport dataExport, Exception exception) { dataExport.setDataExportState(DataExportState.FAILED); dataExport = dataExportRepository.save(dataExport); @@ -141,6 +165,13 @@ private void handleCreationFailure(DataExport dataExport, Exception exception) { mailService.sendDataExportFailedEmailToAdmin(admin.get(), dataExport, exception); } + /** + * Finishes the creation of the data export by setting the file path to the zip file, the state to EMAIL_SENT and the creation finished date. + * + * @param dataExport the data export whose creation is finished + * @param dataExportPath the path to the zip file containing the data export + * @return the updated data export from the database + */ private DataExport finishDataExportCreation(DataExport dataExport, Path dataExportPath) { dataExport.setFilePath(dataExportPath.toString()); dataExport.setCreationFinishedDate(ZonedDateTime.now()); @@ -149,6 +180,15 @@ private DataExport finishDataExportCreation(DataExport dataExport, Path dataExpo return dataExportRepository.save(dataExport); } + /** + * Prepares the data export by creating the working directory, scheduling it for deletion and setting the state to IN_CREATION. + *

+ * If the path where the data exports are stored does not exist yet, it will be created. + * + * @param dataExport the data export to be prepared + * @return the path to the working directory + * @throws IOException if the working directory could not be created + */ private Path prepareDataExport(DataExport dataExport) throws IOException { if (!Files.exists(dataExportsPath)) { Files.createDirectories(dataExportsPath); @@ -161,6 +201,14 @@ private Path prepareDataExport(DataExport dataExport) throws IOException { return workingDirectory; } + /** + * Adds the general user information to the data export. + *

+ * This includes the login, name, email, and registration number (matriculation number). + * + * @param user the user for which the information should be added + * @param workingDirectory the directory in which the information should be stored + */ private void addGeneralUserInformation(User user, Path workingDirectory) throws IOException { String[] headers = { "login", "name", "email", "registration number" }; CSVFormat csvFormat = CSVFormat.DEFAULT.builder().setHeader(headers).build(); @@ -171,11 +219,18 @@ private void addGeneralUserInformation(User user, Path workingDirectory) throws } } + /** + * Creates the zip file containing the data export. + * + * @param userLogin the login of the user for which the data export was created + * @param workingDirectory the directory containing the data export + * @return the path to the zip file + * @throws IOException if the zip file could not be created + */ private Path createDataExportZipFile(String userLogin, Path workingDirectory) throws IOException { // There should actually never exist more than one data export for a user at a time (once the feature is fully implemented), but to be sure the name is unique, we add the // current timestamp return zipFileService.createZipFileWithFolderContent(dataExportsPath.resolve("data-export_" + userLogin + ZonedDateTime.now().toEpochSecond() + ZIP_FILE_EXTENSION), workingDirectory, null); - } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/dataexport/DataExportExamCreationService.java b/src/main/java/de/tum/in/www1/artemis/service/dataexport/DataExportExamCreationService.java index 126c8c204a61..af105f6ba49d 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/dataexport/DataExportExamCreationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/dataexport/DataExportExamCreationService.java @@ -7,9 +7,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -18,14 +16,18 @@ import org.springframework.stereotype.Service; import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.GradingScale; import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.exam.StudentExam; +import de.tum.in.www1.artemis.repository.GradingScaleRepository; import de.tum.in.www1.artemis.repository.StudentExamRepository; import de.tum.in.www1.artemis.service.exam.ExamService; import de.tum.in.www1.artemis.web.rest.dto.ExamScoresDTO; /** * A service to create the data export for exams the user has participated in. + * This includes exercise participations and general information such as working time. + * Results are only included if the results are already published. */ @Service public class DataExportExamCreationService { @@ -38,11 +40,14 @@ public class DataExportExamCreationService { private final ExamService examService; - public DataExportExamCreationService(StudentExamRepository studentExamRepository, DataExportExerciseCreationService dataExportExerciseCreationService, - ExamService examService) { + private final GradingScaleRepository gradingScaleRepository; + + public DataExportExamCreationService(StudentExamRepository studentExamRepository, DataExportExerciseCreationService dataExportExerciseCreationService, ExamService examService, + GradingScaleRepository gradingScaleRepository) { this.studentExamRepository = studentExamRepository; this.dataExportExerciseCreationService = dataExportExerciseCreationService; this.examService = examService; + this.gradingScaleRepository = gradingScaleRepository; } /** @@ -61,14 +66,24 @@ public void createExportForExams(long userId, Path workingDirectory) throws IOEx var exam = studentExam.getExam(); var examTitle = exam.getSanitizedExamTitle(); var courseDirPath = retrieveCourseDirPath(workingDirectory, exam.getCourse()); - createDirectoryIfNotExistent(courseDirPath); + var examsDirPath = courseDirPath.resolve("exams"); + createDirectoryIfNotExistent(examsDirPath); var examDirectoryName = EXAM_DIRECTORY_PREFIX + examTitle + "_" + studentExam.getId(); - var examWorkingDir = Files.createDirectories(courseDirPath.resolve(examDirectoryName)); - createStudentExamExport(studentExam, examWorkingDir); + var examWorkingDirPath = examsDirPath.resolve(examDirectoryName); + createDirectoryIfNotExistent(examWorkingDirPath); + createStudentExamExport(studentExam, examWorkingDirPath); } } } + /** + * Creates the data export for the given student exam. + *

+ * This includes extracting all exercise participations, general exam information such as working time, and the results if the results are published. + * + * @param studentExam the student exam belonging to the user for which the data export should be created + * @param examWorkingDir the directory in which the information about the exam should be stored + */ private void createStudentExamExport(StudentExam studentExam, Path examWorkingDir) throws IOException { for (var exercise : studentExam.getExercises()) { // since the behavior is undefined if multiple student exams for the same exam and student combination exist, the exercise can be null @@ -76,10 +91,10 @@ private void createStudentExamExport(StudentExam studentExam, Path examWorkingDi continue; } if (exercise instanceof ProgrammingExercise programmingExercise) { - dataExportExerciseCreationService.createProgrammingExerciseExport(programmingExercise, examWorkingDir, studentExam.getUser().getId()); + dataExportExerciseCreationService.createProgrammingExerciseExport(programmingExercise, examWorkingDir, studentExam.getUser()); } else { - dataExportExerciseCreationService.createNonProgrammingExerciseExport(exercise, examWorkingDir, studentExam.getUser().getId()); + dataExportExerciseCreationService.createNonProgrammingExerciseExport(exercise, examWorkingDir, studentExam.getUser()); } } // leave out the results if the results are not published yet to avoid leaking information through the data export @@ -89,11 +104,18 @@ private void createStudentExamExport(StudentExam studentExam, Path examWorkingDi addGeneralExamInformation(studentExam, examWorkingDir); } + /** + * Adds the results of the student to the data export. + * + * @param studentExam the student exam for which the results should be added + * @param examWorkingDir the directory in which the results should be stored + */ private void addExamScores(StudentExam studentExam, Path examWorkingDir) throws IOException { var studentExamGrade = examService.getStudentExamGradeForDataExport(studentExam); var studentResult = studentExamGrade.studentResult(); + var gradingScale = gradingScaleRepository.findByExamId(studentExam.getExam().getId()); List headers = new ArrayList<>(); - var examResults = getExamResultsStreamToPrint(studentResult, headers); + var examResults = getExamResultsStreamToPrint(studentResult, headers, gradingScale); CSVFormat csvFormat = CSVFormat.DEFAULT.builder().setHeader(headers.toArray(new String[0])).build(); try (final CSVPrinter printer = new CSVPrinter( Files.newBufferedWriter(examWorkingDir.resolve(EXAM_DIRECTORY_PREFIX + studentExam.getId() + "_result" + CSV_FILE_EXTENSION)), csvFormat)) { @@ -102,21 +124,29 @@ private void addExamScores(StudentExam studentExam, Path examWorkingDir) throws } } - private Stream getExamResultsStreamToPrint(ExamScoresDTO.StudentResult studentResult, List headers) { + /** + * Returns a stream of the exam results that should be included in the exam results CSV file. + * + * @param studentResult the result belonging to the student exam + * @param headers a list containing the column headers that should be included in the CSV file + * @param gradingScaleOptional the optional grading scale of the exam + * @return a stream of information that should be included in the exam results CSV file + */ + private Stream getExamResultsStreamToPrint(ExamScoresDTO.StudentResult studentResult, List headers, Optional gradingScaleOptional) { var builder = Stream.builder(); if (studentResult.overallPointsAchieved() != null) { builder.add(studentResult.overallPointsAchieved()); headers.add("overall points"); } - if (studentResult.hasPassed() != null) { + if (studentResult.hasPassed() != null && gradingScaleOptional.isPresent()) { builder.add(studentResult.hasPassed()); headers.add("passed"); } - if (studentResult.overallGrade() != null) { + if (studentResult.overallGrade() != null && gradingScaleOptional.isPresent()) { builder.add(studentResult.overallGrade()); headers.add("overall grade"); } - if (studentResult.gradeWithBonus() != null) { + if (studentResult.gradeWithBonus() != null && gradingScaleOptional.isPresent()) { builder.add(studentResult.gradeWithBonus()); headers.add("grade with bonus"); } @@ -127,15 +157,63 @@ private Stream getExamResultsStreamToPrint(ExamScoresDTO.StudentResult studen return builder.build(); } + /** + * Adds general information about the student exam to the data export. + *

+ * This includes information such as if the exam was started, if it is a test exam, when it was started, if it was submitted, when it was submitted, the working time, and the + * individual end of the working time. + * + * @param studentExam the student exam for which the information should be added + * @param examWorkingDir the directory in which the information should be stored + */ private void addGeneralExamInformation(StudentExam studentExam, Path examWorkingDir) throws IOException { - String[] headers = { "started", "testExam", "started at", "submitted", "submitted at", "working time (in minutes)", "individual end date" }; - CSVFormat csvFormat = CSVFormat.DEFAULT.builder().setHeader(headers).build(); + List headers = new ArrayList<>(); + var generalExamInformation = getGeneralExamInformationStreamToPrint(studentExam, headers); + CSVFormat csvFormat = CSVFormat.DEFAULT.builder().setHeader(headers.toArray(new String[0])).build(); try (CSVPrinter printer = new CSVPrinter(Files.newBufferedWriter(examWorkingDir.resolve(EXAM_DIRECTORY_PREFIX + studentExam.getId() + CSV_FILE_EXTENSION)), csvFormat)) { - printer.printRecord(studentExam.isStarted(), studentExam.isTestExam(), studentExam.getStartedDate(), studentExam.isSubmitted(), studentExam.getSubmissionDate(), - studentExam.getWorkingTime() / 60, studentExam.getIndividualEndDate()); + printer.printRecord(generalExamInformation); printer.flush(); } } + /** + * Returns a stream of the general exam information that should be included in the general exam information CSV file. + * Do not include information if it is not available, this means null. + * + * @param studentExam the student exam for which the information should be added + * @param headers a list containing the column headers that should be included in the CSV file + * @return a stream of information that should be included in the general exam information CSV file + */ + private Stream getGeneralExamInformationStreamToPrint(StudentExam studentExam, List headers) { + var builder = Stream.builder(); + if (studentExam.isStarted() != null) { + builder.add(studentExam.isStarted()); + headers.add("started"); + } + headers.add("test exam"); + builder.add(studentExam.isTestExam()); + if (studentExam.getStartedDate() != null) { + builder.add(studentExam.getStartedDate()); + headers.add("started at"); + } + if (studentExam.isSubmitted() != null) { + builder.add(studentExam.isSubmitted()); + headers.add("submitted"); + } + if (studentExam.getSubmissionDate() != null) { + builder.add(studentExam.getSubmissionDate()); + headers.add("submitted at"); + } + if (studentExam.getWorkingTime() != null) { + builder.add(studentExam.getWorkingTime() / 60); + headers.add("working time (in minutes)"); + } + if (studentExam.getIndividualEndDate() != null) { + builder.add(studentExam.getIndividualEndDate()); + headers.add("individual end date"); + } + return builder.build(); + } + } diff --git a/src/main/java/de/tum/in/www1/artemis/service/dataexport/DataExportExerciseCreationService.java b/src/main/java/de/tum/in/www1/artemis/service/dataexport/DataExportExerciseCreationService.java index f29034fbf690..73a08df82c95 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/dataexport/DataExportExerciseCreationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/dataexport/DataExportExerciseCreationService.java @@ -1,7 +1,9 @@ package de.tum.in.www1.artemis.service.dataexport; import static de.tum.in.www1.artemis.service.dataexport.DataExportQuizExerciseCreationService.TXT_FILE_EXTENSION; +import static de.tum.in.www1.artemis.service.dataexport.DataExportUtil.createDirectoryIfNotExistent; import static de.tum.in.www1.artemis.service.dataexport.DataExportUtil.retrieveCourseDirPath; +import static de.tum.in.www1.artemis.service.util.RoundingUtil.roundToNDecimalPlaces; import java.io.File; import java.io.IOException; @@ -20,7 +22,7 @@ import org.springframework.stereotype.Service; import de.tum.in.www1.artemis.domain.*; -import de.tum.in.www1.artemis.domain.enumeration.ComplaintType; +import de.tum.in.www1.artemis.domain.enumeration.*; import de.tum.in.www1.artemis.domain.metis.AnswerPost; import de.tum.in.www1.artemis.domain.modeling.ModelingSubmission; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; @@ -29,20 +31,23 @@ import de.tum.in.www1.artemis.repository.ComplaintRepository; import de.tum.in.www1.artemis.repository.ExerciseRepository; import de.tum.in.www1.artemis.repository.plagiarism.PlagiarismCaseRepository; -import de.tum.in.www1.artemis.service.ExerciseDateService; -import de.tum.in.www1.artemis.service.FileService; +import de.tum.in.www1.artemis.service.*; import de.tum.in.www1.artemis.service.connectors.apollon.ApollonConversionService; import de.tum.in.www1.artemis.service.programming.ProgrammingExerciseExportService; import de.tum.in.www1.artemis.web.rest.dto.RepositoryExportOptionsDTO; /** - * A service to create the data export for exercise participations of the user + * A service to create the data export for exercise participations of the user. + * It is responsible for creating the export for programming exercises and modeling, text, and file upload exercises. + * For quiz exercises it delegates the creation of the export to {@link DataExportQuizExerciseCreationService}. */ @Service public class DataExportExerciseCreationService { private static final String PDF_FILE_EXTENSION = ".pdf"; + private static final String EXERCISE_PREFIX = "exercise_"; + static final String CSV_FILE_EXTENSION = ".csv"; private final Path repoClonePath; @@ -64,10 +69,14 @@ public class DataExportExerciseCreationService { private final ExerciseRepository exerciseRepository; + private final ResultService resultService; + + private final AuthorizationCheckService authCheckService; + public DataExportExerciseCreationService(@Value("${artemis.repo-download-clone-path}") Path repoClonePath, FileService fileService, ProgrammingExerciseExportService programmingExerciseExportService, DataExportQuizExerciseCreationService dataExportQuizExerciseCreationService, PlagiarismCaseRepository plagiarismCaseRepository, Optional apollonConversionService, ComplaintRepository complaintRepository, - ExerciseRepository exerciseRepository) { + ExerciseRepository exerciseRepository, ResultService resultService, AuthorizationCheckService authCheckService) { this.fileService = fileService; this.programmingExerciseExportService = programmingExerciseExportService; this.dataExportQuizExerciseCreationService = dataExportQuizExerciseCreationService; @@ -76,53 +85,57 @@ public DataExportExerciseCreationService(@Value("${artemis.repo-download-clone-p this.complaintRepository = complaintRepository; this.exerciseRepository = exerciseRepository; this.repoClonePath = repoClonePath; + this.resultService = resultService; + this.authCheckService = authCheckService; } /** * Creates the export for all exercises the user participated in. * * @param workingDirectory the directory the export should be created in - * @param userId the id of the user that requested the export + * @param user the user for which the export should be created * @throws IOException if an error occurs while accessing the file system */ - public void createExercisesExport(Path workingDirectory, long userId) throws IOException { + public void createExercisesExport(Path workingDirectory, User user) throws IOException { // retrieve all exercises as we cannot retrieve the exercises by course because a user might have participated in a course they are no longer a member of (they have // unenrolled) - var allExerciseParticipations = exerciseRepository.getAllExercisesUserParticipatedInWithEagerParticipationsSubmissionsResultsFeedbacksByUserId(userId); + var allExerciseParticipations = exerciseRepository.getAllExercisesUserParticipatedInWithEagerParticipationsSubmissionsResultsFeedbacksByUserId(user.getId()); var exerciseParticipationsPerCourse = allExerciseParticipations.stream().collect(Collectors.groupingBy(Exercise::getCourseViaExerciseGroupOrCourseMember)); for (var entry : exerciseParticipationsPerCourse.entrySet()) { var course = entry.getKey(); Path courseDir = retrieveCourseDirPath(workingDirectory, course); var exercises = entry.getValue(); + Path exercisesDir = courseDir.resolve("exercises"); if (!exercises.isEmpty()) { - Files.createDirectory(courseDir); + createDirectoryIfNotExistent(exercisesDir); } for (var exercise : exercises) { if (exercise instanceof ProgrammingExercise programmingExercise) { - createProgrammingExerciseExport(programmingExercise, courseDir, userId); + createProgrammingExerciseExport(programmingExercise, exercisesDir, user); } else { - createNonProgrammingExerciseExport(exercise, courseDir, userId); + createNonProgrammingExerciseExport(exercise, exercisesDir, user); } } } } /** - * Creates an export for a given programming exercise. Includes submission information, the repository from the VCS and potential plagiarism cases. + * Creates an export for a given programming exercise. + *

+ * Includes submission information, the repository from the VCS and potential plagiarism cases. * * @param programmingExercise the programming exercise for which the export should be created - * @param courseDir the directory that is used for the course the exercise belongs to - * @param userId the id of the user that requested the export + * @param exercisesDir the directory where all exercises of a course should be stored + * @param user the user for which the export should be created * @throws IOException if an error occurs while accessing the file system */ - - public void createProgrammingExerciseExport(ProgrammingExercise programmingExercise, Path courseDir, long userId) throws IOException { - Path exerciseDir = courseDir.resolve(programmingExercise.getSanitizedExerciseTitle()); + public void createProgrammingExerciseExport(ProgrammingExercise programmingExercise, Path exercisesDir, User user) throws IOException { + Path exerciseDir = exercisesDir.resolve(EXERCISE_PREFIX + programmingExercise.getSanitizedExerciseTitle()); if (!Files.exists(exerciseDir)) { Files.createDirectory(exerciseDir); } - createSubmissionsResultsExport(programmingExercise, exerciseDir); + createSubmissionsResultsExport(programmingExercise, exerciseDir, user); RepositoryExportOptionsDTO repositoryExportOptions = new RepositoryExportOptionsDTO(); repositoryExportOptions.setExportAllParticipants(false); repositoryExportOptions.setAnonymizeRepository(false); @@ -141,7 +154,7 @@ public void createProgrammingExerciseExport(ProgrammingExercise programmingExerc programmingExerciseExportService.exportStudentRepositories(programmingExercise, listOfProgrammingExerciseParticipations, repositoryExportOptions, tempRepoWorkingDir, exerciseDir, Collections.synchronizedList(new ArrayList<>())); - createPlagiarismCaseInfoExport(programmingExercise, exerciseDir, userId); + createPlagiarismCaseInfoExport(programmingExercise, exerciseDir, user.getId()); } @@ -150,21 +163,35 @@ public void createProgrammingExerciseExport(ProgrammingExercise programmingExerc * * @param exercise the exercise for which the export should be created * @param courseDir the directory that is used for the course the exercise belongs to - * @param userId the id of the user that requested the export + * @param user the user for which the export should be created * @throws IOException if an error occurs while accessing the file system */ - public void createNonProgrammingExerciseExport(Exercise exercise, Path courseDir, long userId) throws IOException { - Path exercisePath = courseDir.resolve(exercise.getSanitizedExerciseTitle()); + public void createNonProgrammingExerciseExport(Exercise exercise, Path courseDir, User user) throws IOException { + Path exercisePath = courseDir.resolve(EXERCISE_PREFIX + exercise.getSanitizedExerciseTitle()); if (!Files.exists(exercisePath)) { Files.createDirectory(exercisePath); } - createSubmissionsResultsExport(exercise, exercisePath); - createPlagiarismCaseInfoExport(exercise, exercisePath, userId); + createSubmissionsResultsExport(exercise, exercisePath, user); + createPlagiarismCaseInfoExport(exercise, exercisePath, user.getId()); } - private void createSubmissionsResultsExport(Exercise exercise, Path exerciseDir) throws IOException { - boolean includeResults = exercise.isExamExercise() && exercise.getExamViaExerciseGroupOrCourseMember().resultsPublished() - || exercise.isCourseExercise() && ExerciseDateService.isAfterAssessmentDueDate(exercise); + /** + * Creates the export for the submission of the user to the given exercise. + *

+ * Includes the submission information and the submission content and the results if the results are + * published. + * For quiz exercises it delegates the creation of the export to {@link DataExportQuizExerciseCreationService}. + * + * @param exercise the exercise for which the export should be created + * @param exerciseDir the directory in which the export should be created + * @param user the user for which the export should be created + */ + private void createSubmissionsResultsExport(Exercise exercise, Path exerciseDir, User user) throws IOException { + // quizzes do not have an assessment due date, so we need to check if they have ended according to their due date + boolean isInstructor = authCheckService.isAtLeastInstructorForExercise(exercise, user); + boolean includeResults = (exercise.isExamExercise() && exercise.getExamViaExerciseGroupOrCourseMember().resultsPublished()) + || (exercise.isCourseExercise() && ExerciseDateService.isAfterAssessmentDueDate(exercise) && !(exercise instanceof QuizExercise)) + || (exercise.isCourseExercise() && exercise instanceof QuizExercise quizExercise && quizExercise.isQuizEnded()) || isInstructor; for (var participation : exercise.getStudentParticipations()) { for (var submission : participation.getSubmissions()) { createSubmissionCsvFile(submission, exerciseDir); @@ -180,13 +207,22 @@ else if (submission instanceof ModelingSubmission modelingSubmission) { else if (submission instanceof QuizSubmission) { dataExportQuizExerciseCreationService.createQuizAnswersExport((QuizExercise) exercise, participation, exerciseDir, includeResults); } - if (includeResults) { - createResultsAndComplaintFiles(submission, exerciseDir); + // for a programming exercise, we want to include the results that are visible before the assessment due date + if (includeResults || exercise instanceof ProgrammingExercise) { + boolean programmingExerciseBeforeAssessmentDueDate = exercise instanceof ProgrammingExercise && !ExerciseDateService.isAfterAssessmentDueDate(exercise); + createResultsAndComplaintFiles(submission, exerciseDir, user, programmingExerciseBeforeAssessmentDueDate, isInstructor); } } } } + /** + * Stores the modeling submission as pdf if the apollon profile is active and the apollon conversion service works, otherwise stores it as json file. + * + * @param modelingSubmission the modeling submission for which the content should be stored + * @param outputDir the directory in which the content should be stored + * @throws IOException if the file cannot be written + */ private void storeModelingSubmissionContent(ModelingSubmission modelingSubmission, Path outputDir) throws IOException { if (modelingSubmission.getModel() == null) { log.warn("Cannot include modeling submission content in data export because content is null for submission with id: {}", modelingSubmission.getId()); @@ -202,12 +238,22 @@ private void storeModelingSubmissionContent(ModelingSubmission modelingSubmissio try (var modelAsPdf = apollonConversionService.get().convertModel(modelingSubmission.getModel())) { Files.write(outputDir.resolve(fileName + PDF_FILE_EXTENSION), modelAsPdf.readAllBytes()); } - catch (IOException e) { + catch (Exception e) { log.warn("Failed to include the model as pdf, going to include it as plain JSON file."); addModelJsonWithExplanationHowToView(modelingSubmission.getModel(), outputDir, fileName); } } + /** + * Stores the given model as json file and adds a markdown file with an explanation how to view the model. + *

+ * Used if the Apollon Conversion Service is not available or an error occurs while using it. + * + * @param model the model belonging to the submission as JSON string + * @param outputDir the directory in which the content should be stored + * @param fileName the file name of the JSON file + * @throws IOException if the file cannot be written + */ private void addModelJsonWithExplanationHowToView(String model, Path outputDir, String fileName) throws IOException { Files.writeString(outputDir.resolve(fileName + ".json"), model); String explanation = """ @@ -216,6 +262,13 @@ private void addModelJsonWithExplanationHowToView(String model, Path outputDir, Files.writeString(outputDir.resolve("view_model.md"), explanation); } + /** + * Stores the text submission content as txt file. + * + * @param textSubmission the text submission for which the content should be stored + * @param outputDir the directory in which the content should be stored + * @throws IOException if the file cannot be written + */ private void storeTextSubmissionContent(TextSubmission textSubmission, Path outputDir) throws IOException { // text can be null which leads to an exception if (textSubmission.getText() != null) { @@ -226,32 +279,56 @@ private void storeTextSubmissionContent(TextSubmission textSubmission, Path outp } } - private void createResultsAndComplaintFiles(Submission submission, Path outputDir) throws IOException { + /** + * Creates a txt file containing the results with the score, the number of passed test cases if it is a programming exercise + * and the feedbacks (both manual and automatic). + * + * @param submission the submission for which the results should be stored + * @param outputDir the directory in which the results should be stored + * @param user the user for which the export should be created + * @param programmingExerciseBeforeAssessmentDueDate whether the programming exercise is before the assessment due date + * @param isInstructor whether the user is an instructor in the course the exercise belongs to + * @throws IOException if the file cannot be written + */ + private void createResultsAndComplaintFiles(Submission submission, Path outputDir, User user, boolean programmingExerciseBeforeAssessmentDueDate, boolean isInstructor) + throws IOException { StringBuilder resultScoreAndFeedbacks = new StringBuilder(); for (var result : submission.getResults()) { if (result != null) { + // Do not include the results if the assessment due date is in the future and the assessment is not automatic and the user is not an instructor + // We only consider programming exercises here because for other exercises this method is not called if the assessment due date is in the future + if (programmingExerciseBeforeAssessmentDueDate && result.getAssessmentType() != AssessmentType.AUTOMATIC && !isInstructor) { + continue; + } + resultService.filterSensitiveInformationIfNecessary(submission.getParticipation(), List.of(result), Optional.of(user)); var score = result.getScore(); if (score != null) { resultScoreAndFeedbacks.append("Score of submission: ").append(score).append("%").append(" ") - .append(score * submission.getParticipation().getExercise().getMaxPoints() / 100).append(" Points").append("\n"); + .append(roundToNDecimalPlaces(score * submission.getParticipation().getExercise().getMaxPoints() / 100, 2)).append(" Points").append("\n"); } if (submission instanceof ProgrammingSubmission && result.getPassedTestCaseCount() != null && result.getTestCaseCount() != null && result.getTestCaseCount() > 0) { resultScoreAndFeedbacks.append("Passed test cases: ").append(result.getPassedTestCaseCount()).append("/").append(result.getTestCaseCount()).append("\n"); } + if (submission instanceof ProgrammingSubmission programmingSubmission && programmingSubmission.isBuildFailed()) { + resultScoreAndFeedbacks.append("Build failed").append("\n"); + } for (var feedback : result.getFeedbacks()) { - resultScoreAndFeedbacks.append("- Feedback: "); - // null if it's manual feedback - if (feedback.getText() != null) { - resultScoreAndFeedbacks.append(feedback.getText()).append("\t"); + if (feedback != null) { + resultScoreAndFeedbacks.append("- Feedback: "); + + // null if it's manual feedback + if (feedback.getText() != null) { + resultScoreAndFeedbacks.append(feedback.getText()).append("\t"); + } + // null if the test case passes + if (feedback.getDetailText() != null) { + resultScoreAndFeedbacks.append(feedback.getDetailText()).append("\t"); + } + if (feedback.getCredits() != null) { + resultScoreAndFeedbacks.append(feedback.getCredits()); + } + resultScoreAndFeedbacks.append("\n"); } - // null if the test case passes - if (feedback.getDetailText() != null) { - resultScoreAndFeedbacks.append(feedback.getDetailText()).append("\t"); - } - if (feedback.getCredits() != null) { - resultScoreAndFeedbacks.append(feedback.getCredits()); - } - resultScoreAndFeedbacks.append("\n"); } Files.writeString(outputDir.resolve("submission_" + submission.getId() + "_result_" + result.getId() + TXT_FILE_EXTENSION), resultScoreAndFeedbacks); } @@ -263,6 +340,15 @@ private void createResultsAndComplaintFiles(Submission submission, Path outputDi } } + /** + * Creates a CSV file containing the complaint data. + *

+ * Complaint can be either a complaint or a more feedback request. + * + * @param complaint the complaint for which the data should be stored + * @param outputDir the directory in which the data should be stored + * @throws IOException if the file cannot be written + */ private void addComplaintData(Complaint complaint, Path outputDir) throws IOException { List headers = new ArrayList<>(); var dataStreamBuilder = Stream.builder(); @@ -292,6 +378,14 @@ private void addComplaintData(Complaint complaint, Path outputDir) throws IOExce } } + /** + * Creates a CSV file containing the plagiarism case information. + * + * @param exercise the exercise for which the plagiarism case information should be stored + * @param exercisePath the directory in which the plagiarism case information should be stored + * @param userId the id of the user that requested the export and that is involved in the plagiarism case + * @throws IOException if the file cannot be written + */ private void createPlagiarismCaseInfoExport(Exercise exercise, Path exercisePath, long userId) throws IOException { var plagiarismCaseOptional = plagiarismCaseRepository.findByStudentIdAndExerciseIdWithPostAndAnswerPost(userId, exercise.getId()); List headers = new ArrayList<>(); @@ -329,6 +423,14 @@ else if (plagiarismCase.getVerdict() == PlagiarismVerdict.WARNING) { } } + /** + * Copies the file upload submission file to the data export working directory if it still exists. + * + * @param submissionFilePath the path to the file upload submission file + * @param outputDir the directory to which the file should be copied + * @param fileUploadSubmission the file upload submission for which the file should be copied + * @throws IOException if the file cannot be copied + */ private void copyFileUploadSubmissionFile(String submissionFilePath, Path outputDir, FileUploadSubmission fileUploadSubmission) throws IOException { try { FileUtils.copyDirectory(new File(submissionFilePath), outputDir.toFile()); @@ -339,12 +441,28 @@ private void copyFileUploadSubmissionFile(String submissionFilePath, Path output } } + /** + * Adds a markdown file to the data export working directory that informs the user that the file for the file upload submission no longer exists. + * + * @param outputDir the directory in which the file should be stored + * @param fileUploadSubmission the file upload submission for which the file should be stored + * @throws IOException if the file cannot be written + */ private void addInfoThatFileForFileUploadSubmissionNoLongerExists(Path outputDir, FileUploadSubmission fileUploadSubmission) throws IOException { var exercise = fileUploadSubmission.getParticipation().getExercise(); Files.writeString(outputDir.resolve("submission_file_no_longer_exists.md"), String.format("Your submitted file for the exercise %s no longer exists on the file system.", exercise)); } + /** + * Creates a CSV file containing the submission information. + *

+ * This includes the id, the submission date and the commit hash if it is a programming exercise. + * + * @param submission the submission for which the information should be stored + * @param outputPath the directory in which the information should be stored + * @throws IOException if the file cannot be written + */ private void createSubmissionCsvFile(Submission submission, Path outputPath) throws IOException { List headers = new ArrayList<>(List.of("id", "submissionDate")); if (submission instanceof ProgrammingSubmission) { @@ -360,6 +478,14 @@ private void createSubmissionCsvFile(Submission submission, Path outputPath) thr } } + /** + * Returns a stream of the submission information that should be included in the CSV file. + *

+ * This includes the id, the submission date and the commit hash if it is a programming exercise. + * + * @param submission the submission for which the information should be stored + * @return a stream of the submission information that should be included in the CSV file + */ private Stream getSubmissionStreamToPrint(Submission submission) { var builder = Stream.builder(); builder.add(submission.getId()).add(submission.getSubmissionDate()); diff --git a/src/main/java/de/tum/in/www1/artemis/service/dataexport/DataExportQuizExerciseCreationService.java b/src/main/java/de/tum/in/www1/artemis/service/dataexport/DataExportQuizExerciseCreationService.java index bcc98cd6dedb..6b51e2ecf0ff 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/dataexport/DataExportQuizExerciseCreationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/dataexport/DataExportQuizExerciseCreationService.java @@ -16,7 +16,10 @@ import de.tum.in.www1.artemis.service.DragAndDropQuizAnswerConversionService; /** - * A service to create the data export for quiz exercise participations + * A service to create the data export for quiz exercise participations. + * This includes creating a pdf highlighting the submitted answers for drag and drop questions and + * txt files containing the submitted answers for multiple choice and short answer questions. + * Additionally, the results can be included in the export if the due date is over. */ @Service public class DataExportQuizExerciseCreationService { @@ -80,6 +83,13 @@ else if (submittedAnswer instanceof MultipleChoiceSubmittedAnswer multipleChoice } + /** + * Creates a txt file containing the submitted answers for a multiple choice question and information if the answer was correct or not if includeResults is true. + * + * @param multipleChoiceSubmittedAnswer the submitted answer to a multiple choice question that should be included in the export + * @param includeResults true if the results should be included in the export (if the assessment due date or result publication date is over) + * @return the content for the txt file as a string + */ private String createExportForMultipleChoiceAnswerQuestion(MultipleChoiceSubmittedAnswer multipleChoiceSubmittedAnswer, boolean includeResults) { StringBuilder stringBuilder = new StringBuilder(); MultipleChoiceQuestion question = (MultipleChoiceQuestion) multipleChoiceSubmittedAnswer.getQuizQuestion(); @@ -105,6 +115,15 @@ private String createExportForMultipleChoiceAnswerQuestion(MultipleChoiceSubmitt return stringBuilder.toString(); } + /** + * Adds an explanation to the answer option if no result should be included in the export. + *

+ * The explanation contains information if the answer option was selected or not or if it is invalid. + * + * @param multipleChoiceSubmittedAnswer the submitted answer to a multiple choice question that should be included in the export + * @param stringBuilder the string builder user to create the txt file content + * @param answerOption the answer option for which the explanation should be added + */ private void addExplanationToAnswerOptionWithoutResult(MultipleChoiceSubmittedAnswer multipleChoiceSubmittedAnswer, StringBuilder stringBuilder, AnswerOption answerOption) { if (answerOption.isInvalid()) { stringBuilder.append("Invalid answer option: "); @@ -117,6 +136,15 @@ else if (multipleChoiceSubmittedAnswer.getSelectedOptions().contains(answerOptio } } + /** + * Adds an explanation to the answer option if a result should be included in the export. + *

+ * The explanation contains information if the answer option was selected or not or it is invalid and if the answer option is correct or not. + * + * @param multipleChoiceSubmittedAnswer the submitted answer to a multiple choice question that should be included in the export + * @param stringBuilder the string builder user to create the txt file content + * @param answerOption the answer option for which the explanation should be added + */ private void addExplanationToAnswerOptionWithResult(MultipleChoiceSubmittedAnswer multipleChoiceSubmittedAnswer, StringBuilder stringBuilder, AnswerOption answerOption) { if (answerOption.isInvalid()) { stringBuilder.append("Invalid answer option: "); @@ -135,6 +163,13 @@ else if (!answerOption.isIsCorrect() && !multipleChoiceSubmittedAnswer.getSelect } } + /** + * Creates a txt file containing the submitted answers for a short answer question and information if the answer was correct or not if includeResults is true. + * + * @param shortAnswerSubmittedAnswer the submitted answer to a short answer question that should be included in the export + * @param includeResults true if the results should be included in the export (if the assessment due date or result publication date is over) + * @return the content for the txt file as a string + */ private String createExportForShortAnswerQuestion(ShortAnswerSubmittedAnswer shortAnswerSubmittedAnswer, boolean includeResults) { StringBuilder stringBuilder = new StringBuilder(); ShortAnswerQuestion question = (ShortAnswerQuestion) shortAnswerSubmittedAnswer.getQuizQuestion(); @@ -145,6 +180,14 @@ private String createExportForShortAnswerQuestion(ShortAnswerSubmittedAnswer sho return replaceSpotWithSubmittedAnswer(shortAnswerSubmittedAnswer, stringBuilder, includeResults); } + /** + * Replaces the spots (the gaps that indicate where an answer should be entered) in the text of a short answer question with the submitted answers. + * + * @param shortAnswerSubmittedAnswer the submitted answer to a short answer question that should be included in the export + * @param submittedAnswer the string builder user to create the txt file content + * @param includeResults true if the results should be included in the export (if the assessment due date or result publication date is over) + * @return the string containing the question text with the answers of the user + */ private String replaceSpotWithSubmittedAnswer(ShortAnswerSubmittedAnswer shortAnswerSubmittedAnswer, StringBuilder submittedAnswer, boolean includeResults) { var spotToSubmittedTextMap = buildMapFromSpotsToSubmittedAnswers(shortAnswerSubmittedAnswer); submittedAnswer.append("Your answer: ").append("\n"); @@ -160,30 +203,39 @@ private String replaceSpotWithSubmittedAnswer(ShortAnswerSubmittedAnswer shortAn return submittedAnswer.toString(); } + /** + * Adds the submitted answer to the string builder if the answer is correct or incorrect. + * + * @param submittedAnswer the string builder user to create the txt file content + * @param includeResults true if the results should be included in the export (if the assessment due date or result publication date is over) + * @param submittedText the question text of a short answer question that should be included in the export + * @param pattern the pattern used to find the spot in the question text + * @param matcher the matcher used to find the spot in the question text + * @param replacement the string builder used to create the replacement (the submitted answer text) for the spot + * @return the matcher used to find the next spot in the question text + */ private Matcher addSubmittedAnswerWithResult(StringBuilder submittedAnswer, boolean includeResults, ShortAnswerSubmittedText submittedText, Pattern pattern, Matcher matcher, StringBuilder replacement) { int start = matcher.start(); int end = matcher.end(); - if (submittedText.isIsCorrect() != null && submittedText.isIsCorrect()) { - replacement.append(submittedText.getText()); - if (includeResults) { - replacement.append(" (Correct)"); - } + replacement.append(submittedText.getText()); + if (submittedText.isIsCorrect() != null && submittedText.isIsCorrect() && includeResults) { + replacement.append(" (Correct)"); } - else if (submittedText.isIsCorrect() != null && !submittedText.isIsCorrect()) { - replacement.append(submittedText.getText()); - if (includeResults) { - replacement.append(" (Incorrect)"); - } - else { - replacement.append(submittedText.getText()); - } - submittedAnswer.replace(start, end, replacement.toString()); - matcher = pattern.matcher(submittedAnswer); + else if (submittedText.isIsCorrect() != null && !submittedText.isIsCorrect() && includeResults) { + replacement.append(" (Incorrect)"); } + submittedAnswer.replace(start, end, replacement.toString()); + matcher = pattern.matcher(submittedAnswer); return matcher; } + /** + * Builds a map from the spots (the gaps that indicate where an answer should be entered) in the text of a short answer question to the submitted answers. + * + * @param shortAnswerSubmittedAnswer the submitted answer to a short answer question that should be included in the export + * @return a map from the spots (represented as the string in text) to the submitted answers + */ private Map buildMapFromSpotsToSubmittedAnswers(ShortAnswerSubmittedAnswer shortAnswerSubmittedAnswer) { Map spotsToSubmittedAnswers = new HashMap<>(); for (var submittedText : shortAnswerSubmittedAnswer.getSubmittedTexts()) { diff --git a/src/main/java/de/tum/in/www1/artemis/service/dataexport/DataExportService.java b/src/main/java/de/tum/in/www1/artemis/service/dataexport/DataExportService.java index c23d99dc241f..a067a4eeb301 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/dataexport/DataExportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/dataexport/DataExportService.java @@ -22,10 +22,13 @@ import de.tum.in.www1.artemis.service.FileService; import de.tum.in.www1.artemis.web.rest.dto.DataExportDTO; import de.tum.in.www1.artemis.web.rest.dto.RequestDataExportDTO; +import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException; import de.tum.in.www1.artemis.web.rest.errors.InternalServerErrorException; /** * Service Implementation for managing the data export in accordance with Art. 15 GDPR. + * This service is responsible for downloading, deleting data exports and checking if a data export can be requested. + * For creating data exports, see {@link DataExportCreationService}. */ @Service public class DataExportService { @@ -109,8 +112,7 @@ public Resource downloadDataExport(DataExport dataExport) { public DataExportDTO canDownloadAnyDataExport() { var noDataExport = new DataExportDTO(null, null, null, null); var user = userRepository.getUser(); - var dataExportsFromUser = dataExportRepository.findAllDataExportsByUserId(user.getId()); - Optional latestDataExport = dataExportsFromUser.stream().max(Comparator.comparing(DataExport::getCreatedDate)); + var dataExportsFromUser = dataExportRepository.findAllDataExportsByUserIdOrderByRequestDateDesc(user.getId()); if (dataExportsFromUser.isEmpty()) { return noDataExport; } @@ -120,10 +122,21 @@ public DataExportDTO canDownloadAnyDataExport() { return new DataExportDTO(dataExport.getId(), dataExport.getDataExportState(), dataExport.getCreatedDate().atZone(ZoneId.systemDefault()), nextRequestDate); } } - return new DataExportDTO(null, latestDataExport.get().getDataExportState(), latestDataExport.get().getCreatedDate().atZone(ZoneId.systemDefault()), - retrieveNextRequestDate(latestDataExport.get())); + var latestDataExport = dataExportsFromUser.get(0); + return new DataExportDTO(null, latestDataExport.getDataExportState(), latestDataExport.getCreatedDate().atZone(ZoneId.systemDefault()), + retrieveNextRequestDate(latestDataExport)); } + /** + * Calculates the next date when the user can request a data export. + *

+ * This is the date when the last data export was requested (stored in the createdDate) + the constant DAYS_BETWEEN_DATA_EXPORTS. + * By default, DAYS_BETWEEN_DATA_EXPORTS is set to 14 days. + * This can be changed by setting the property artemis.data-export.days-between-data-exports in the application.yml file. + * + * @param dataExport the data export for which the next request date should be calculated + * @return the next date when the user can request a data export + */ @NotNull private ZonedDateTime retrieveNextRequestDate(DataExport dataExport) { return dataExport.getCreatedDate().atZone(ZoneId.systemDefault()).plusDays(DAYS_BETWEEN_DATA_EXPORTS); @@ -148,4 +161,18 @@ public void deleteDataExportAndSetDataExportState(DataExport dataExport) { dataExportRepository.save(dataExport); } + /** + * Checks if the data export can be downloaded. + *

+ * The data export can be downloaded if its state is either EMAIL_SENT or DOWNLOADED. + * + * @param dataExport the data export to check + * @throws AccessForbiddenException if the data export is not in a downloadable state + */ + public void checkDataExportCanBeDownloadedElseThrow(DataExport dataExport) { + if (!dataExport.getDataExportState().isDownloadable()) { + throw new AccessForbiddenException("Data export has either not been created or already been deleted"); + } + } + } diff --git a/src/main/java/de/tum/in/www1/artemis/service/dataexport/DataExportUtil.java b/src/main/java/de/tum/in/www1/artemis/service/dataexport/DataExportUtil.java index 8c711062d08c..b275ec861b7e 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/dataexport/DataExportUtil.java +++ b/src/main/java/de/tum/in/www1/artemis/service/dataexport/DataExportUtil.java @@ -17,12 +17,25 @@ private DataExportUtil() { // Utility class } + /** + * Creates the given directory if it does not exist yet. + * + * @param directory the directory to create + * @throws IOException if an error occurs while accessing the file system + */ static void createDirectoryIfNotExistent(Path directory) throws IOException { if (!Files.exists(directory)) { - Files.createDirectory(directory); + Files.createDirectories(directory); } } + /** + * Retrieves the path to the directory for the given course within the data export. + * + * @param workingDirectory the working directory where the data export is created + * @param course the course for which the directory should be retrieved + * @return the path to the directory for the given course + */ static Path retrieveCourseDirPath(Path workingDirectory, Course course) { return workingDirectory.resolve(COURSE_DIRECTORY_PREFIX + course.getShortName()); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamUserService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamUserService.java index f60e68c2a921..cd2aabaaad9f 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamUserService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamUserService.java @@ -6,6 +6,7 @@ import java.util.List; import java.util.Optional; +import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.text.PDFTextStripperByArea; import org.slf4j.Logger; @@ -51,7 +52,7 @@ public ExamUserService(FileService fileService, UserRepository userRepository, E */ public List parsePDF(MultipartFile file) { - try (PDDocument document = PDDocument.load(file.getBytes())) { + try (PDDocument document = Loader.loadPDF(file.getBytes())) { ImageExtractor imageExtractor = new ImageExtractor(document); imageExtractor.process(); List images = imageExtractor.getImages(); @@ -129,6 +130,6 @@ public ExamUsersNotFoundDTO saveImages(long examId, MultipartFile file) { /** * Contains the information about an exam user with image */ - private record ExamUserWithImageDTO(String studentRegistrationNumber, ImageDTO image) { + record ExamUserWithImageDTO(String studentRegistrationNumber, ImageDTO image) { } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ImageExtractor.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ImageExtractor.java index 232c98a373a2..f7f9f210a2d0 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/ImageExtractor.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ImageExtractor.java @@ -46,12 +46,12 @@ public ImageExtractor(PDDocument document) { this.images = new ArrayList<>(); this.pdfDocument = document; - addOperator(new Concatenate()); - addOperator(new DrawObject()); - addOperator(new SetGraphicsStateParameters()); - addOperator(new Save()); - addOperator(new Restore()); - addOperator(new SetMatrix()); + addOperator(new Concatenate(this)); + addOperator(new DrawObject(this)); + addOperator(new SetGraphicsStateParameters(this)); + addOperator(new Save(this)); + addOperator(new Restore(this)); + addOperator(new SetMatrix(this)); } /** diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseExportService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseExportService.java index ac7bed156470..de1a21404714 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseExportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseExportService.java @@ -50,7 +50,8 @@ import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; import de.tum.in.www1.artemis.domain.enumeration.RepositoryType; -import de.tum.in.www1.artemis.domain.participation.*; +import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; +import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.exception.GitException; import de.tum.in.www1.artemis.repository.AuxiliaryRepositoryRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; @@ -90,7 +91,9 @@ public class ProgrammingExerciseExportService { public static final String EXPORTED_EXERCISE_PROBLEM_STATEMENT_FILE_PREFIX = "Problem-Statement"; - private static final String EMBEDDED_FILE_REGEX = "\\[.*] *\\(/api/files/markdown/.*\\)"; + private static final String EMBEDDED_FILE_MARKDOWN_SYNTAX_REGEX = "\\[.*] *\\(/api/files/markdown/.*\\)"; + + private static final String EMBEDDED_FILE_HTML_SYNTAX_REGEX = ""; private static final String API_MARKDOWN_FILE_PATH = "/api/files/markdown/"; @@ -152,6 +155,14 @@ public Path exportProgrammingExerciseInstructorMaterial(ProgrammingExercise exer return pathToZippedExercise; } + /** + * Export problem statement and embedded files for a given programming exercise. + * + * @param exercise the programming exercise that is exported + * @param exportErrors List of failures that occurred during the export + * @param exportDir the directory where the content of the export is stored + * @param pathsToBeZipped the paths that should be included in the zip file + */ private void exportProblemStatementAndEmbeddedFiles(ProgrammingExercise exercise, List exportErrors, Path exportDir, List pathsToBeZipped) { var problemStatementFileExtension = ".md"; String problemStatementFileName = EXPORTED_EXERCISE_PROBLEM_STATEMENT_FILE_PREFIX + "-" + exercise.getTitle() + problemStatementFileExtension; @@ -168,15 +179,97 @@ private void exportProblemStatementAndEmbeddedFiles(ProgrammingExercise exercise * @param outputDir the directory where the content of the export is stored * @param pathsToBeZipped the paths that should be included in the zip file */ - private void copyEmbeddedFiles(ProgrammingExercise exercise, Path outputDir, List pathsToBeZipped, List exportErrors) { - Set embeddedFiles = new HashSet<>(); + Set embeddedFilesWithMarkdownSyntax = new HashSet<>(); + Set embeddedFilesWithHtmlSyntax = new HashSet<>(); + + Matcher matcherForMarkdownSyntax = Pattern.compile(EMBEDDED_FILE_MARKDOWN_SYNTAX_REGEX).matcher(exercise.getProblemStatement()); + Matcher matcherForHtmlSyntax = Pattern.compile(EMBEDDED_FILE_HTML_SYNTAX_REGEX).matcher(exercise.getProblemStatement()); + checkForMatchesInProblemStatementAndCreateDirectoryForFiles(outputDir, pathsToBeZipped, exportErrors, embeddedFilesWithMarkdownSyntax, matcherForMarkdownSyntax); + Path embeddedFilesDir = checkForMatchesInProblemStatementAndCreateDirectoryForFiles(outputDir, pathsToBeZipped, exportErrors, embeddedFilesWithHtmlSyntax, + matcherForHtmlSyntax); + // if the returned path is null the directory could not be created + if (embeddedFilesDir == null) { + return; + } + copyFilesEmbeddedWithMarkdownSyntax(exercise, exportErrors, embeddedFilesWithMarkdownSyntax, embeddedFilesDir); + copyFilesEmbeddedWithHtmlSyntax(exercise, exportErrors, embeddedFilesWithHtmlSyntax, embeddedFilesDir); + + } + + /** + * Copies the files that are embedded with Markdown syntax to the embedded files' directory. + * + * @param exercise the programming exercise that is exported + * @param exportErrors List of failures that occurred during the export + * @param embeddedFilesWithMarkdownSyntax the files that are embedded with Markdown syntax + * @param embeddedFilesDir the directory where the embedded files are stored + */ + private void copyFilesEmbeddedWithMarkdownSyntax(ProgrammingExercise exercise, List exportErrors, Set embeddedFilesWithMarkdownSyntax, Path embeddedFilesDir) { + for (String embeddedFile : embeddedFilesWithMarkdownSyntax) { + // avoid matching other closing ] or () in the squared brackets by getting the index of the last ] + String lastPartOfMatchedString = embeddedFile.substring(embeddedFile.lastIndexOf("]") + 1); + String filePath = lastPartOfMatchedString.substring(lastPartOfMatchedString.indexOf("(") + 1, lastPartOfMatchedString.indexOf(")")); + constructFilenameAndCopyFile(exercise, exportErrors, embeddedFilesDir, filePath); + } + } + + /** + * Copies the files that are embedded with html syntax to the embedded files' directory. + * + * @param exercise the programming exercise that is exported + * @param exportErrors List of failures that occurred during the export + * @param embeddedFilesWithHtmlSyntax the files that are embedded with html syntax + * @param embeddedFilesDir the directory where the embedded files are stored + */ + private void copyFilesEmbeddedWithHtmlSyntax(ProgrammingExercise exercise, List exportErrors, Set embeddedFilesWithHtmlSyntax, Path embeddedFilesDir) { + for (String embeddedFile : embeddedFilesWithHtmlSyntax) { + int indexOfFirstQuotationMark = embeddedFile.indexOf('"'); + String filePath = embeddedFile.substring(embeddedFile.indexOf("src=") + 5, embeddedFile.indexOf('"', indexOfFirstQuotationMark + 1)); + constructFilenameAndCopyFile(exercise, exportErrors, embeddedFilesDir, filePath); + } + } - Matcher matcher = Pattern.compile(EMBEDDED_FILE_REGEX).matcher(exercise.getProblemStatement()); + /** + * Extracts the filename from the matched string and copies the file to the embedded files' directory. + * + * @param exercise the programming exercise that is exported + * @param exportErrors List of failures that occurred during the export + * @param embeddedFilesDir the directory where the embedded files are stored + * @param filePath the path of the file that should be copied + */ + private void constructFilenameAndCopyFile(ProgrammingExercise exercise, List exportErrors, Path embeddedFilesDir, String filePath) { + String fileName = filePath.replace(API_MARKDOWN_FILE_PATH, ""); + Path imageFilePath = Path.of(FilePathService.getMarkdownFilePath(), fileName); + Path imageExportPath = embeddedFilesDir.resolve(fileName); + // we need this check as it might be that the matched string is different and not filtered out above but the file is already copied + if (!Files.exists(imageExportPath)) { + try { + Files.copy(imageFilePath, imageExportPath); + } + catch (IOException e) { + exportErrors.add("Failed to copy embedded files: " + e.getMessage()); + log.warn("Could not copy embedded file {} for exercise with id {}", fileName, exercise.getId()); + } + } + } + + /** + * Checks for matches in the problem statement and creates a directory for the embedded files. + * + * @param outputDir the directory where the content of the export is stored + * @param pathsToBeZipped the paths that should be included in the zip file + * @param exportErrors List of failures that occurred during the export + * @param embeddedFiles the files that are embedded in the problem statement + * @param matcher the matcher that is used to find the embedded files + * @return the path to the embedded files directory or null if the directory could not be created + */ + private Path checkForMatchesInProblemStatementAndCreateDirectoryForFiles(Path outputDir, List pathsToBeZipped, List exportErrors, Set embeddedFiles, + Matcher matcher) { while (matcher.find()) { embeddedFiles.add(matcher.group()); } - log.debug("Found embedded files:{} ", embeddedFiles); + log.debug("Found embedded files: {} ", embeddedFiles); Path embeddedFilesDir = outputDir.resolve("files"); if (!embeddedFiles.isEmpty()) { if (!Files.exists(embeddedFilesDir)) { @@ -186,30 +279,12 @@ private void copyEmbeddedFiles(ProgrammingExercise exercise, Path outputDir, Lis catch (IOException e) { exportErrors.add("Could not create directory for embedded files: " + e.getMessage()); log.warn("Could not create directory for embedded files. Won't include embedded files: " + e.getMessage()); - return; + return null; } } pathsToBeZipped.add(embeddedFilesDir); } - for (String embeddedFile : embeddedFiles) { - // avoid matching other closing ] or () in the squared brackets by getting the index of the last ] - String lastPartOfMatchedString = embeddedFile.substring(embeddedFile.lastIndexOf("]") + 1); - String filePath = lastPartOfMatchedString.substring(lastPartOfMatchedString.indexOf("(") + 1, lastPartOfMatchedString.indexOf(")")); - String fileName = filePath.replace(API_MARKDOWN_FILE_PATH, ""); - Path imageFilePath = Path.of(FilePathService.getMarkdownFilePath(), fileName); - Path imageExportPath = embeddedFilesDir.resolve(fileName); - // we need this check as it might be that the matched string is different and not filtered out above but the file is already copied - if (!Files.exists(imageExportPath)) { - try { - Files.copy(imageFilePath, imageExportPath); - } - catch (IOException e) { - exportErrors.add("Failed to copy embedded files: " + e.getMessage()); - log.warn("Could not copy embedded file {} for exercise with id {}", fileName, exercise.getId()); - } - } - } - + return embeddedFilesDir; } /** diff --git a/src/main/java/de/tum/in/www1/artemis/service/scheduled/DataExportScheduleService.java b/src/main/java/de/tum/in/www1/artemis/service/scheduled/DataExportScheduleService.java index b856eac57890..8278387c1efe 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/scheduled/DataExportScheduleService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/scheduled/DataExportScheduleService.java @@ -1,8 +1,8 @@ package de.tum.in.www1.artemis.service.scheduled; -import java.util.HashSet; -import java.util.Optional; -import java.util.Set; +import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -57,18 +57,19 @@ public DataExportScheduleService(DataExportRepository dataExportRepository, Data * Deleted will be all data exports that have a creation date older than seven days */ @Scheduled(cron = "${artemis.scheduling.data-export-creation-time: 0 0 4 * * *}") - public void createDataExportsAndDeleteOldOnes() { + public void createDataExportsAndDeleteOldOnes() throws InterruptedException { if (profileService.isDev()) { // do not execute this in a development environment // NOTE: if you want to test this locally, please comment it out, but do not commit the changes return; } - checkSecurityUtils(); log.info("Creating data exports and deleting old ones"); - Set successfulDataExports = new HashSet<>(); + Set successfulDataExports = Collections.synchronizedSet(new HashSet<>()); var dataExportsToBeCreated = dataExportRepository.findAllToBeCreated(); - dataExportsToBeCreated.forEach(dataExport -> createDataExport(dataExport, successfulDataExports)); + ExecutorService executor = Executors.newFixedThreadPool(10); + dataExportsToBeCreated.forEach(dataExport -> executor.execute(() -> createDataExport(dataExport, successfulDataExports))); + executor.shutdown(); var dataExportsToBeDeleted = dataExportRepository.findAllToBeDeleted(); dataExportsToBeDeleted.forEach(this::deleteDataExport); Optional admin = userService.findInternalAdminUser(); @@ -76,6 +77,12 @@ public void createDataExportsAndDeleteOldOnes() { log.warn("No internal admin user found. Cannot send email to admin about successful creation of data exports."); return; } + // This job runs at 4 am by default and the next scheduled job runs at 5 am, so we should allow 60 minutes for the creation. + // If the creation doesn't finish within 60 minutes, all pending exports will be picked up when the job runs the next time. + if (!executor.awaitTermination(60, java.util.concurrent.TimeUnit.MINUTES)) { + log.info("Not all pending data exports could be created within 60 minutes."); + executor.shutdownNow(); + } if (!successfulDataExports.isEmpty()) { mailService.sendSuccessfulDataExportsEmailToAdmin(admin.get(), successfulDataExports); } @@ -87,6 +94,7 @@ public void createDataExportsAndDeleteOldOnes() { * @param dataExport the data export to be created */ private void createDataExport(DataExport dataExport, Set successfulDataExports) { + checkSecurityUtils(); log.info("Creating data export for {}", dataExport.getUser().getLogin()); var successful = dataExportCreationService.createDataExport(dataExport); if (successful) { diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/DataExportResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/DataExportResource.java index be14fd560b6e..366a915199ef 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/DataExportResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/DataExportResource.java @@ -5,14 +5,12 @@ import java.time.Duration; import java.time.ZoneId; import java.time.ZonedDateTime; -import java.util.Comparator; import javax.validation.constraints.NotNull; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.Resource; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; +import org.springframework.http.*; import org.springframework.web.bind.annotation.*; import de.tum.in.www1.artemis.domain.DataExport; @@ -27,7 +25,8 @@ import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; /** - * REST controller for data exports + * REST controller for data exports. + * It contains the REST endpoints for requesting, downloading data exports and checking if a data export can be requested or downloaded. */ @RestController @RequestMapping("api/") @@ -50,9 +49,9 @@ public DataExportResource(@Value("${artemis.data-export.days-between-data-export } /** - * Request a data export for the given user + * POST /data-exports: Request a data export for the currently logged-in user. * - * @return the data export object + * @return a DTO containing the id of the data export that was created, its state and when it was requested */ @PostMapping("data-exports") @EnforceAtLeastStudent @@ -65,16 +64,19 @@ public RequestDataExportDTO requestDataExport() { /** * Checks if the user can request a new data export. + *

+ * This is the case if the user has not requested a data export yet or if the last data export was created more than DAYS_BETWEEN_DATA_EXPORTS days ago. * * @return true if the user can request a new data export, false otherwise */ private boolean canRequestDataExport() { var user = userRepository.getUser(); - var dataExports = dataExportRepository.findAllDataExportsByUserId(user.getId()); + var dataExports = dataExportRepository.findAllDataExportsByUserIdOrderByRequestDateDesc(user.getId()); if (dataExports.isEmpty()) { return true; } - var latestDataExport = dataExports.stream().max(Comparator.comparing(DataExport::getCreatedDate)).get(); + // because we order by request date desc, the first data export is the latest one + var latestDataExport = dataExports.get(0); var olderThanDaysBetweenDataExports = Duration.between(latestDataExport.getCreatedDate().atZone(ZoneId.systemDefault()), ZonedDateTime.now()) .toDays() >= DAYS_BETWEEN_DATA_EXPORTS; @@ -82,7 +84,12 @@ private boolean canRequestDataExport() { } /** - * Download the data export for the given user + * GET /data-exports/{dataExportId}: Download the data export for the given id. + *

+ * We check if the user is the owner of the data export and if the data export can be downloaded. + * If this is the case, we return a resource containing the data export zip file. + * The file name is set to the name of the zip file. + * The content disposition header is set to attachment so that the browser will download the file instead of displaying it. * * @param dataExportId the id of the data export to download * @return A resource containing the data export zip file @@ -92,20 +99,18 @@ private boolean canRequestDataExport() { public ResponseEntity downloadDataExport(@PathVariable long dataExportId) { DataExport dataExport = dataExportRepository.findByIdElseThrow(dataExportId); currentlyLoggedInUserIsOwnerOfDataExportElseThrow(dataExport); - checkDataExportCanBeDownloaded(dataExport); + dataExportService.checkDataExportCanBeDownloadedElseThrow(dataExport); Resource resource = dataExportService.downloadDataExport(dataExport); File finalZipFile = Path.of(dataExport.getFilePath()).toFile(); - return ResponseEntity.ok().contentLength(finalZipFile.length()).contentType(MediaType.APPLICATION_OCTET_STREAM).header("filename", finalZipFile.getName()).body(resource); - } - - private void checkDataExportCanBeDownloaded(DataExport dataExport) { - if (!dataExport.getDataExportState().isDownloadable()) { - throw new AccessForbiddenException("Data export has either not been created or already been deleted"); - } + ContentDisposition contentDisposition = ContentDisposition.builder("attachment").filename(finalZipFile.getName()).build(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentDisposition(contentDisposition); + return ResponseEntity.ok().contentLength(finalZipFile.length()).headers(headers).contentType(MediaType.APPLICATION_OCTET_STREAM).header("filename", finalZipFile.getName()) + .body(resource); } /** - * checks if the currently logged-in user is the owner of the given data export + * Checks if the currently logged-in user is the owner of the given data export. * * @param dataExport the data export that needs to be checked * @throws AccessForbiddenException if logged-in user isn't the owner of the data export @@ -117,7 +122,7 @@ private void currentlyLoggedInUserIsOwnerOfDataExportElseThrow(@NotNull DataExpo } /** - * checks if the currently logged-in user is owner of the given data export + * Checks if the currently logged-in user is owner of the given data export. * * @param dataExport the data export that needs to be checked * @return true if the user is the owner of the data export, false otherwise @@ -132,7 +137,7 @@ private boolean currentlyLoggedInUserIsOwnerOfDataExport(DataExport dataExport) } /** - * Check if the user can request a data export + * GET /data-exports/can-request: Check if the logged-in user can request a data export. * * @return true if the user can request a data export, false otherwise */ @@ -143,9 +148,9 @@ public boolean canRequestExport() { } /** - * Check if the user can download any data export + * GET /data-exports/can-download: Check if the logged-in user can download any data export. * - * @return a data export DTO with the id of the export that can be downloaded or a DTO with a id of null if no export can be downloaded + * @return a data export DTO with the id of the export that can be downloaded or a DTO with an id of null if no export can be downloaded */ @GetMapping("data-exports/can-download") @EnforceAtLeastStudent @@ -154,7 +159,7 @@ public DataExportDTO canDownloadAnyExport() { } /** - * Check if the user can download a specific data export + * GET /data-exports/{dataExportId}/can-download: Check if the logged-in user can download the data export with the given id. * * @param dataExportId the id of the data export that should be checked * @return true if the user can download the data export, false otherwise @@ -170,8 +175,8 @@ public boolean canDownloadSpecificExport(@PathVariable long dataExportId) { * * @param dataExportId the id of the data export to check * @return true if the data export can be downloaded, false otherwise - * @throws de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException if the data export or the user could not be found - * @throws de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException if the user is not allowed to download the data export + * @throws EntityNotFoundException if the data export or the user could not be found + * @throws AccessForbiddenException if the user is not allowed to download the data export */ private boolean canDownloadSpecificDataExport(long dataExportId) throws EntityNotFoundException, AccessForbiddenException { var dataExport = dataExportRepository.findByIdElseThrow(dataExportId); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseParticipationResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseParticipationResource.java index 19b31e24c79a..e7a545e69e6d 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseParticipationResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseParticipationResource.java @@ -83,7 +83,7 @@ public ResponseEntity getParticipationW participationAuthCheckService.checkCanAccessParticipationElseThrow(participation); // hide details that should not be shown to the students - resultService.filterSensitiveInformationIfNecessary(participation, participation.getResults()); + resultService.filterSensitiveInformationIfNecessary(participation, participation.getResults(), Optional.empty()); return ResponseEntity.ok(participation); } diff --git a/src/main/webapp/app/core/legal/data-export/data-export.component.html b/src/main/webapp/app/core/legal/data-export/data-export.component.html index 182a57931dd1..693f7d2fe7d6 100644 --- a/src/main/webapp/app/core/legal/data-export/data-export.component.html +++ b/src/main/webapp/app/core/legal/data-export/data-export.component.html @@ -24,7 +24,8 @@

id="download-data-export-btn" [btnSize]="ButtonSize.LARGE" [disabled]="!canDownload" - title="artemisApp.dataExport.download" + [tooltip]="'artemisApp.dataExport.download'" + [title]="'artemisApp.dataExport.download'" (onClick)="downloadDataExport()" > @@ -33,7 +34,7 @@

{{ 'artemisApp.dataExport.lastRequestDate' | artemisTranslate }} {{ dataExport?.createdDate | artemisDate: 'long-date' }}

-

+

{{ 'artemisApp.dataExport.nextRequestDate' | artemisTranslate }} {{ dataExport?.nextRequestDate | artemisDate: 'long-date' }}

diff --git a/src/main/webapp/app/core/legal/data-export/data-export.component.ts b/src/main/webapp/app/core/legal/data-export/data-export.component.ts index f1e34d48705f..37b5de87c4e7 100644 --- a/src/main/webapp/app/core/legal/data-export/data-export.component.ts +++ b/src/main/webapp/app/core/legal/data-export/data-export.component.ts @@ -4,9 +4,8 @@ import { Subject } from 'rxjs'; import { ButtonSize, ButtonType } from 'app/shared/components/button.component'; import { DataExportService } from 'app/core/legal/data-export/data-export.service'; import { AccountService } from 'app/core/auth/account.service'; -import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; +import { HttpErrorResponse } from '@angular/common/http'; import { AlertService } from 'app/core/util/alert.service'; -import { downloadZipFileFromResponse } from 'app/shared/util/download.util'; import { DataExport, DataExportState } from 'app/entities/data-export.model'; import { ActivatedRoute } from '@angular/router'; import { convertDateFromServer } from 'app/utils/date.utils'; @@ -96,10 +95,7 @@ export class DataExportComponent implements OnInit { } downloadDataExport() { - this.dataExportService.downloadDataExport(this.dataExportId).subscribe((response: HttpResponse) => { - downloadZipFileFromResponse(response); - this.alertService.success('artemisApp.dataExport.downloadSuccess'); - }); + this.dataExportService.downloadDataExport(this.dataExportId); } requestExportForAnotherUser(login: string) { diff --git a/src/main/webapp/app/core/legal/data-export/data-export.service.ts b/src/main/webapp/app/core/legal/data-export/data-export.service.ts index 6f405a9034f0..f7b2ce5a8527 100644 --- a/src/main/webapp/app/core/legal/data-export/data-export.service.ts +++ b/src/main/webapp/app/core/legal/data-export/data-export.service.ts @@ -1,4 +1,4 @@ -import { HttpClient, HttpResponse } from '@angular/common/http'; +import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { DataExport } from 'app/entities/data-export.model'; @@ -11,11 +11,9 @@ export class DataExportService { return this.http.post(`api/data-exports`, {}); } - downloadDataExport(dataExportId: number): Observable> { - return this.http.get(`api/data-exports/${dataExportId}`, { - observe: 'response', - responseType: 'blob', - }); + downloadDataExport(dataExportId: number) { + const url = `api/data-exports/${dataExportId}`; + window.open(url, '_blank'); } canRequestDataExport(): Observable { diff --git a/src/main/webapp/app/exam/participate/exam-participation.module.ts b/src/main/webapp/app/exam/participate/exam-participation.module.ts index d57cef937a87..3ba594471fd3 100644 --- a/src/main/webapp/app/exam/participate/exam-participation.module.ts +++ b/src/main/webapp/app/exam/participate/exam-participation.module.ts @@ -29,8 +29,8 @@ import { ArtemisHeaderExercisePageWithDetailsModule } from 'app/exercises/shared import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; import { FileUploadExamSubmissionComponent } from 'app/exam/participate/exercises/file-upload/file-upload-exam-submission.component'; import { ExamExerciseOverviewPageComponent } from 'app/exam/participate/exercises/exercise-overview-page/exam-exercise-overview-page.component'; -import { ExamExerciseUpdateHighlighterComponent } from 'app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.component'; import { SubmissionResultStatusModule } from 'app/overview/submission-result-status.module'; +import { ExamExerciseUpdateHighlighterModule } from 'app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.module'; const ENTITY_STATES = [...examParticipationState]; @@ -55,6 +55,7 @@ const ENTITY_STATES = [...examParticipationState]; ArtemisParticipationSummaryModule, ArtemisMarkdownModule, SubmissionResultStatusModule, + ExamExerciseUpdateHighlighterModule, ], declarations: [ ExamParticipationComponent, @@ -67,7 +68,6 @@ const ENTITY_STATES = [...examParticipationState]; ExamNavigationBarComponent, ExamTimerComponent, ExamExerciseOverviewPageComponent, - ExamExerciseUpdateHighlighterComponent, ], }) export class ArtemisExamParticipationModule {} diff --git a/src/main/webapp/app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.component.ts b/src/main/webapp/app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.component.ts index 9938725296e7..0b607e7ec78b 100644 --- a/src/main/webapp/app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.component.ts +++ b/src/main/webapp/app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.component.ts @@ -1,7 +1,7 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { Subscription } from 'rxjs'; import { ExamExerciseUpdateService } from 'app/exam/manage/exam-exercise-update.service'; -import { Exercise } from 'app/entities/exercise.model'; +import { Exercise, ExerciseType } from 'app/entities/exercise.model'; import { Diff, DiffMatchPatch, DiffOperation } from 'diff-match-patch-typescript'; @Component({ @@ -9,13 +9,13 @@ import { Diff, DiffMatchPatch, DiffOperation } from 'diff-match-patch-typescript templateUrl: './exam-exercise-update-highlighter.component.html', styleUrls: ['./exam-exercise-update-highlighter.component.scss'], }) -export class ExamExerciseUpdateHighlighterComponent implements OnInit { +export class ExamExerciseUpdateHighlighterComponent implements OnInit, OnDestroy { subscriptionToLiveExamExerciseUpdates: Subscription; + themeSubscription: Subscription; previousProblemStatementUpdate: string; updatedProblemStatementWithHighlightedDifferences: string; updatedProblemStatement: string; showHighlightedDifferences = true; - @Input() exercise: Exercise; @Output() problemStatementUpdateEvent: EventEmitter = new EventEmitter(); @@ -28,6 +28,11 @@ export class ExamExerciseUpdateHighlighterComponent implements OnInit { }); } + ngOnDestroy(): void { + this.subscriptionToLiveExamExerciseUpdates?.unsubscribe(); + this.themeSubscription?.unsubscribe(); + } + /** * Switches the view between the new(updated) problem statement without the difference * with the view showing the difference between the new and old problem statement and vice versa. @@ -79,14 +84,49 @@ export class ExamExerciseUpdateHighlighterComponent implements OnInit { } this.previousProblemStatementUpdate = this.updatedProblemStatement; - + let removedDiagrams: string[] = []; + let diff: Diff[]; + if (this.exercise.type === ExerciseType.PROGRAMMING) { + const updatedProblemStatementAndRemovedDiagrams = this.removeAnyPlantUmlDiagramsInProblemStatement(this.updatedProblemStatement); + const outdatedProblemStatementAndRemovedDiagrams = this.removeAnyPlantUmlDiagramsInProblemStatement(outdatedProblemStatement); + const updatedProblemStatementWithoutDiagrams = updatedProblemStatementAndRemovedDiagrams.problemStatementWithoutPlantUmlDiagrams; + const outdatedProblemStatementWithoutDiagrams = outdatedProblemStatementAndRemovedDiagrams.problemStatementWithoutPlantUmlDiagrams; + removedDiagrams = updatedProblemStatementAndRemovedDiagrams.removedDiagrams; + diff = dmp.diff_main(outdatedProblemStatementWithoutDiagrams!, updatedProblemStatementWithoutDiagrams); + } else { + diff = dmp.diff_main(outdatedProblemStatement!, this.updatedProblemStatement); + } // finds the initial difference then cleans the text with added html & css elements - const diff = dmp.diff_main(outdatedProblemStatement!, this.updatedProblemStatement); dmp.diff_cleanupEfficiency(diff); this.updatedProblemStatementWithHighlightedDifferences = this.diffPrettyHtml(diff); + + if (this.exercise.type === ExerciseType.PROGRAMMING) { + this.addPlantUmlToProblemStatementWithDiffHighlightAgain(removedDiagrams); + } return this.updatedProblemStatementWithHighlightedDifferences; } + private addPlantUmlToProblemStatementWithDiffHighlightAgain(removedDiagrams: string[]) { + removedDiagrams.forEach((text) => { + this.updatedProblemStatementWithHighlightedDifferences = this.updatedProblemStatementWithHighlightedDifferences.replace('@startuml', '@startuml\n' + text + '\n'); + }); + } + + private removeAnyPlantUmlDiagramsInProblemStatement(problemStatement: string): { problemStatementWithoutPlantUmlDiagrams: string; removedDiagrams: string[] } { + // Regular expression to match content between @startuml and @enduml + const plantUmlSequenceRegex = /@startuml([\s\S]*?)@enduml/g; + const removedDiagrams: string[] = []; + const problemStatementWithoutPlantUmlDiagrams = problemStatement.replace(plantUmlSequenceRegex, (match, content) => { + removedDiagrams.push(content); + // we have to keep the markers, otherwise we cannot add the diagrams back later + return '@startuml\n@enduml'; + }); + return { + problemStatementWithoutPlantUmlDiagrams, + removedDiagrams, + }; + } + /** * Convert a diff array into a pretty HTML report. * Keeps markdown styling intact (not like the original method) @@ -98,17 +138,17 @@ export class ExamExerciseUpdateHighlighterComponent implements OnInit { * @param diffs Array of diff tuples. (from DiffMatchPatch) * @return the HTML representation as string with markdown intact. */ - diffPrettyHtml = function (diffs: Diff[]): string { + private diffPrettyHtml(diffs: Diff[]): string { const html: any[] = []; diffs.forEach((diff: Diff, index: number) => { const op = diffs[index][0]; // Operation (insert, delete, equal) const text = diffs[index][1]; // Text of change. switch (op) { case DiffOperation.DIFF_INSERT: - html[index] = '' + text + ''; + html[index] = '' + text + ''; break; case DiffOperation.DIFF_DELETE: - html[index] = '' + text + ''; + html[index] = '' + text + ''; break; case DiffOperation.DIFF_EQUAL: html[index] = text; @@ -116,5 +156,5 @@ export class ExamExerciseUpdateHighlighterComponent implements OnInit { } }); return html.join(''); - }; + } } diff --git a/src/main/webapp/app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.module.ts b/src/main/webapp/app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.module.ts new file mode 100644 index 000000000000..30e5255744b1 --- /dev/null +++ b/src/main/webapp/app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.module.ts @@ -0,0 +1,10 @@ +import { NgModule } from '@angular/core'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; +import { ExamExerciseUpdateHighlighterComponent } from 'app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.component'; + +@NgModule({ + declarations: [ExamExerciseUpdateHighlighterComponent], + imports: [ArtemisSharedCommonModule], + exports: [ExamExerciseUpdateHighlighterComponent], +}) +export class ExamExerciseUpdateHighlighterModule {} diff --git a/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise-management.module.ts b/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise-management.module.ts index 0b704a7bc039..2e0795a8afb2 100644 --- a/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise-management.module.ts +++ b/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise-management.module.ts @@ -20,6 +20,7 @@ import { NonProgrammingExerciseDetailCommonActionsModule } from 'app/exercises/s import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { ExerciseCategoriesModule } from 'app/shared/exercise-categories/exercise-categories.module'; import { ExerciseTitleChannelNameModule } from 'app/exercises/shared/exercise-title-channel-name/exercise-title-channel-name.module'; +import { ExerciseUpdateNotificationModule } from 'app/exercises/shared/exercise-update-notification/exercise-update-notification.module'; @NgModule({ imports: [ @@ -41,6 +42,7 @@ import { ExerciseTitleChannelNameModule } from 'app/exercises/shared/exercise-ti ArtemisSharedComponentModule, ExerciseCategoriesModule, ExerciseTitleChannelNameModule, + ExerciseUpdateNotificationModule, ], declarations: [FileUploadExerciseComponent, FileUploadExerciseDetailComponent, FileUploadExerciseUpdateComponent], exports: [FileUploadExerciseComponent], diff --git a/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise-update.component.html b/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise-update.component.html index e4740153ec82..71b190057fb9 100644 --- a/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise-update.component.html +++ b/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise-update.component.html @@ -197,10 +197,7 @@

Assessment Instructions -
- - -
+
-
- - -
+