diff --git a/.github/workflows/analysis-of-endpoint-connections.yml b/.github/workflows/analysis-of-endpoint-connections.yml deleted file mode 100644 index f74dff1b7b95..000000000000 --- a/.github/workflows/analysis-of-endpoint-connections.yml +++ /dev/null @@ -1,117 +0,0 @@ -name: Analysis of Endpoint Connections - -on: - workflow_dispatch: - pull_request: - types: - - opened - - synchronize - paths: - - 'src/main/java/**' - - 'src/main/webapp/**' - -# Keep in sync with build.yml and test.yml and codeql-analysis.yml -env: - CI: true - node: 20 - java: 21 - -jobs: - Parse-rest-calls-and-endpoints: - timeout-minutes: 10 - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up JDK - uses: actions/setup-java@v4 - with: - java-version: '${{ env.java }}' - distribution: 'temurin' - cache: 'gradle' - - - name: Set up node.js - uses: actions/setup-node@v4 - with: - node-version: '${{ env.node }}' - - - name: Parse client sided REST-API calls - run: | - npm install - tsc -p supporting_scripts/analysis-of-endpoint-connections/src/main/typeScript/tsconfig.analysisOfEndpointConnections.json - node supporting_scripts/analysis-of-endpoint-connections/src/main/typeScript/AnalysisOfEndpointConnectionsClient.js - - - name: Parse server sided Endpoints - run: ./gradlew :supporting_scripts:analysis-of-endpoint-connections:runEndpointParser - - - name: Upload parsing results - uses: actions/upload-artifact@v4 - with: - name: REST API Parsing Results - path: | - supporting_scripts/analysis-of-endpoint-connections/endpoints.json - supporting_scripts/analysis-of-endpoint-connections/restCalls.json - - Analysis-of-endpoint-connections: - needs: Parse-rest-calls-and-endpoints - timeout-minutes: 10 - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up JDK - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '${{ env.java }}' - cache: 'gradle' - - - name: Download JSON files - uses: actions/download-artifact@v4 - with: - name: REST API Parsing Results - path: supporting_scripts/analysis-of-endpoint-connections/ - - - name: Analyze endpoints - run: | - ./gradlew :supporting_scripts:analysis-of-endpoint-connections:runEndpointAnalysis - continue-on-error: true - id: endpointAnalysis - - - name: Analyze rest calls - run: | - ./gradlew :supporting_scripts:analysis-of-endpoint-connections:runRestCallAnalysis - continue-on-error: true - id: restCallAnalysis - - - name: Upload analysis results - uses: actions/upload-artifact@v4 - with: - name: Endpoint and REST Call Analysis Results - path: | - supporting_scripts/analysis-of-endpoint-connections/endpointAnalysisResult.json - supporting_scripts/analysis-of-endpoint-connections/restCallAnalysisResult.json - - - name: Check if any step failed - run: | - if [ "${{ steps.endpointAnalysis.outcome }}" != "success" ] && - [ "${{ steps.restCallAnalysis.outcome }}" != "success" ]; then - echo "Endpoints and REST calls could not be matched." - exit 1 - fi - if [ "${{ steps.endpointAnalysis.outcome }}" == "success" ] && - [ "${{ steps.restCallAnalysis.outcome }}" != "success" ]; then - echo "REST calls could not be matched." - exit 1 - fi - if [ "${{ steps.endpointAnalysis.outcome }}" != "success" ] && - [ "${{ steps.restCallAnalysis.outcome }}" == "success" ]; then - echo "Endpoints could not be matched." - exit 1 - fi diff --git a/.gitignore b/.gitignore index 8f71a8ae13d5..75cb003dda0c 100644 --- a/.gitignore +++ b/.gitignore @@ -219,4 +219,3 @@ data-exports/ # Supporting scripts config ############################## /supporting_scripts/**/*.ini -/supporting_scripts/analysis-of-endpoint-connections/build/**/* diff --git a/README.md b/README.md index ccd260fa5144..106eaa13709a 100644 --- a/README.md +++ b/README.md @@ -193,7 +193,7 @@ Refer to [Using JHipster in production](http://www.jhipster.tech/production) for The following command can automate the deployment to a server. The example shows the deployment to the main Artemis test server (which runs a virtual machine): ```shell -./artemis-server-cli deploy username@artemistest.ase.in.tum.de -w build/libs/Artemis-7.7.4.war +./artemis-server-cli deploy username@artemistest.ase.in.tum.de -w build/libs/Artemis-7.7.5.war ``` ## Architecture diff --git a/build.gradle b/build.gradle index 8c830f0784f8..4baffa384852 100644 --- a/build.gradle +++ b/build.gradle @@ -20,12 +20,12 @@ plugins { id "com.github.ben-manes.versions" version "0.51.0" id "com.github.andygoossens.modernizer" version "${modernizer_plugin_version}" id "com.gorylenko.gradle-git-properties" version "2.4.2" - id "org.owasp.dependencycheck" version "11.1.0" + id "org.owasp.dependencycheck" version "11.1.1" id "com.adarshr.test-logger" version "4.0.0" } group = "de.tum.cit.aet.artemis" -version = "7.7.4" +version = "7.7.5" description = "Interactive Learning with Individual Feedback" java { @@ -80,7 +80,7 @@ spotless { } } importOrderFile "artemis-spotless.importorder" - eclipse("4.28").configFile "artemis-spotless-style.xml" + eclipse("4.33").configFile "artemis-spotless-style.xml" removeUnusedImports() trimTrailingWhitespace() @@ -96,9 +96,10 @@ spotless { @Override String apply(String s, File file) throws Exception { if (s =~ /\nimport .*\*;/) { - throw new AssertionError("Do not use wildcard imports. spotlessApply cannot resolve this issue.\n" + + throw new IllegalArgumentException("Do not use wildcard imports. spotlessApply cannot resolve this issue.\n" + "The following file violates this rule: " + file.getName()) } + return s // Ensure a value is returned after processing } })) } @@ -134,8 +135,8 @@ test { } testLogging.showStandardStreams = true reports.html.required = false - minHeapSize = "1024m" // initial heap size - maxHeapSize = "3072m" // maximum heap size + minHeapSize = "2g" // initial heap size + maxHeapSize = "8g" // maximum heap size } tasks.register("testReport", TestReport) { @@ -180,13 +181,13 @@ jacocoTestCoverageVerification { counter = "INSTRUCTION" value = "COVEREDRATIO" // TODO: in the future the following value should become higher than 0.92 - minimum = 0.895 + minimum = 0.892 } limit { counter = "CLASS" value = "MISSEDCOUNT" // TODO: in the future the following value should become less than 10 - maximum = 60 + maximum = 65 } } } @@ -212,28 +213,16 @@ repositories { maven { url "https://build.shibboleth.net/maven/releases" } - // required for latest jgit 7.0.0 dependency - // TODO: remove this when jgit is available in the official maven repository - maven { - url "https://repo.eclipse.org/content/repositories/jgit-releases" - } } -ext["jackson.version"] = fasterxml_version -ext["junit-jupiter.version"] = junit_version - -ext { qDoxVersionReusable = "com.thoughtworks.qdox:qdox:2.1.0" } -ext { springBootStarterWeb = "org.springframework.boot:spring-boot-starter-web:${spring_boot_version}" } - dependencies { // Note: jenkins-client is not well maintained and includes dependencies to libraries with critical security issues (e.g. CVE-2020-10683 for dom4j@1.6.1) // implementation "com.offbytwo.jenkins:jenkins-client:0.3.8" implementation files("libs/jenkins-client-0.4.1.jar") // The following 4 dependencies are explicitly integrated as transitive dependencies of jenkins-client-0.4.0.jar - // NOTE: we cannot upgrade to the latest version for org.apache.httpcomponents because of exceptions in Docker Java - implementation "org.apache.httpcomponents.client5:httpclient5:5.3.1" // also used by Docker Java - implementation "org.apache.httpcomponents.core5:httpcore5:5.2.5" + implementation "org.apache.httpcomponents.client5:httpclient5:5.4.1" + implementation "org.apache.httpcomponents.core5:httpcore5:5.3.1" implementation "org.apache.httpcomponents:httpmime:4.5.14" implementation("org.dom4j:dom4j:2.1.4") { // Note: avoid org.xml.sax.SAXNotRecognizedException: unrecognized feature http://xml.org/sax/features/external-general-entities @@ -246,7 +235,7 @@ dependencies { exclude module: "jaxb-api" } - implementation "org.gitlab4j:gitlab4j-api:6.0.0-rc.6" + implementation "org.gitlab4j:gitlab4j-api:6.0.0-rc.7" implementation "de.jplag:jplag:${jplag_version}" @@ -268,7 +257,7 @@ dependencies { implementation "org.apache.lucene:lucene-queryparser:${lucene_version}" implementation "org.apache.lucene:lucene-core:${lucene_version}" implementation "org.apache.lucene:lucene-analyzers-common:${lucene_version}" - implementation "com.google.protobuf:protobuf-java:4.28.3" + implementation "com.google.protobuf:protobuf-java:4.29.1" // we have to override those values to use the latest version implementation "org.slf4j:jcl-over-slf4j:${slf4j_version}" @@ -279,7 +268,7 @@ dependencies { } } - implementation "org.apache.logging.log4j:log4j-to-slf4j:2.24.1" + implementation "org.apache.logging.log4j:log4j-to-slf4j:2.24.2" // Note: spring-security-lti13 does not work with jakarta yet, so we built our own custom version and declare its transitive dependencies below // implementation "uk.ac.ox.ctl:spring-security-lti13:0.1.11" @@ -299,7 +288,7 @@ dependencies { implementation "org.apache.sshd:sshd-sftp:${sshd_version}" // https://mvnrepository.com/artifact/net.sourceforge.plantuml/plantuml - implementation "net.sourceforge.plantuml:plantuml:1.2024.7" + implementation "net.sourceforge.plantuml:plantuml:1.2024.8" implementation "org.jasypt:jasypt:1.9.3" implementation "me.xdrop:fuzzywuzzy:1.4.0" implementation("org.yaml:snakeyaml") { @@ -309,7 +298,7 @@ dependencies { } } - implementation qDoxVersionReusable + implementation "com.thoughtworks.qdox:qdox:2.2.0" implementation "io.sentry:sentry-logback:${sentry_version}" implementation "io.sentry:sentry-spring-boot-starter-jakarta:${sentry_version}" @@ -327,24 +316,22 @@ dependencies { // required by Saml2 implementation "org.apache.santuario:xmlsec:4.0.3" - implementation "org.jsoup:jsoup:1.18.1" + implementation "org.jsoup:jsoup:1.18.3" implementation "commons-codec:commons-codec:1.17.1" // needed for spring security saml2 - // TODO: decide if we want to use OpenAPI and Swagger v3 -// implementation 'io.swagger.core.v3:swagger-annotations:2.2.23' -// implementation "org.springdoc:springdoc-openapi-ui:1.8.0" - // use the latest version to avoid security vulnerabilities - implementation "org.springframework:spring-webmvc:6.1.14" + implementation "org.springframework:spring-webmvc:${spring_framework_version}" implementation "com.vdurmont:semver4j:3.1.0" implementation "com.github.docker-java:docker-java-core:${docker_java_version}" - implementation "com.github.docker-java:docker-java-transport-httpclient5:${docker_java_version}" + // Note: we explicitly use docker-java-transport-zerodep, because docker-java-transport-httpclient5 uses an outdated http5 version which is not compatible with Spring Boot >= 3.4.0 + implementation "com.github.docker-java:docker-java-transport-zerodep:${docker_java_version}" // use newest version of commons-compress to avoid security issues through outdated dependencies implementation "org.apache.commons:commons-compress:1.27.1" + // import JHipster dependencies BOM implementation platform("tech.jhipster:jhipster-dependencies:${jhipster_dependencies_version}") @@ -393,7 +380,7 @@ dependencies { implementation "org.springframework.boot:spring-boot-starter-aop:${spring_boot_version}" implementation "org.springframework.boot:spring-boot-starter-data-jpa:${spring_boot_version}" implementation "org.springframework.boot:spring-boot-starter-security:${spring_boot_version}" - implementation(springBootStarterWeb) { + implementation("org.springframework.boot:spring-boot-starter-web:${spring_boot_version}") { exclude module: "spring-boot-starter-undertow" } implementation "org.springframework.boot:spring-boot-starter-tomcat:${spring_boot_version}" @@ -403,24 +390,19 @@ dependencies { implementation "org.springframework.boot:spring-boot-starter-oauth2-client:${spring_boot_version}" implementation "org.springframework.ldap:spring-ldap-core:3.2.8" - implementation "org.springframework.data:spring-data-ldap:3.3.5" + implementation "org.springframework.data:spring-data-ldap:3.4.0" - implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client:4.1.3") { + implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client:${spring_cloud_version}") { // NOTE: these modules contain security vulnerabilities and are not needed exclude module: "commons-jxpath" exclude module: "woodstox-core" } - implementation "org.springframework.cloud:spring-cloud-starter-config:4.1.3" - implementation "org.springframework.cloud:spring-cloud-commons:4.1.4" + implementation "org.springframework.cloud:spring-cloud-starter-config:${spring_cloud_version}" + implementation "org.springframework.cloud:spring-cloud-commons:${spring_cloud_version}" + implementation "io.netty:netty-all:4.1.115.Final" implementation "io.projectreactor.netty:reactor-netty:1.2.0" - implementation("io.netty:netty-common") { - version { - strictly netty_version - } - } - - implementation "org.springframework:spring-messaging:6.1.14" + implementation "org.springframework:spring-messaging:${spring_framework_version}" implementation "org.springframework.retry:spring-retry:2.0.10" implementation "org.springframework.security:spring-security-config:${spring_security_version}" @@ -428,7 +410,6 @@ dependencies { implementation "org.springframework.security:spring-security-core:${spring_security_version}" implementation "org.springframework.security:spring-security-oauth2-core:${spring_security_version}" implementation "org.springframework.security:spring-security-oauth2-client:${spring_security_version}" - implementation "org.springframework.security:spring-security-oauth2-resource-server:${spring_security_version}" // use newest version of nimbus-jose-jwt to avoid security issues through outdated dependencies implementation "com.nimbusds:nimbus-jose-jwt:9.47" @@ -547,25 +528,17 @@ dependencies { testImplementation "org.gradle:gradle-tooling-api:8.11.1" testImplementation "org.apache.maven.surefire:surefire-report-parser:3.5.2" testImplementation "com.opencsv:opencsv:5.9" - testImplementation("io.zonky.test:embedded-database-spring-test:2.5.1") { + testImplementation("io.zonky.test:embedded-database-spring-test:2.6.0") { exclude group: "org.testcontainers", module: "mariadb" exclude group: "org.testcontainers", module: "mssqlserver" } - testImplementation "org.testcontainers:testcontainers:${testcontainer_version}" - testImplementation "org.testcontainers:mysql:${testcontainer_version}" - testImplementation "org.testcontainers:postgresql:${testcontainer_version}" - testImplementation "org.testcontainers:testcontainers:${testcontainer_version}" - testImplementation "org.testcontainers:junit-jupiter:${testcontainer_version}" - testImplementation "org.testcontainers:jdbc:${testcontainer_version}" - testImplementation "org.testcontainers:database-commons:${testcontainer_version}" - testImplementation "com.tngtech.archunit:archunit:1.3.0" testImplementation("org.skyscreamer:jsonassert:1.5.3") { exclude module: "android-json" } - // cannot update due to "Syntax error in SQL statement "WITH ids_to_delete" -// testImplementation "com.h2database:h2:2.3.230" + // NOTE: cannot update due to "Syntax error in SQL statement "WITH ids_to_delete" --> should be resolved when we collapse the changelogs again for Artemis 8.0 +// testImplementation "com.h2database:h2:2.3.232" testImplementation "com.h2database:h2:2.2.224" // Lightweight JSON library needed for the internals of the MockRestServiceServer @@ -577,7 +550,7 @@ dependencies { dependencyManagement { imports { - mavenBom "io.zonky.test.postgres:embedded-postgres-binaries-bom:17.0.0" + mavenBom "io.zonky.test.postgres:embedded-postgres-binaries-bom:17.2.0" } } @@ -634,7 +607,7 @@ tasks.withType(Test).configureEach { } wrapper { - gradleVersion = "8.11" + gradleVersion = "8.11.1" } tasks.register("stage") { diff --git a/docs/dev/guidelines/client-tests.rst b/docs/dev/guidelines/client-tests.rst index 30733fec2d0f..947ec2fe8fe9 100644 --- a/docs/dev/guidelines/client-tests.rst +++ b/docs/dev/guidelines/client-tests.rst @@ -18,10 +18,9 @@ The most basic test looks similar to this: let someComponentFixture: ComponentFixture; let someComponent: SomeComponent; - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [], - declarations: [ + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ SomeComponent, MockPipe(SomePipeUsedInTemplate), MockComponent(SomeComponentUsedInTemplate), @@ -31,11 +30,10 @@ The most basic test looks similar to this: MockProvider(SomeServiceUsedInComponent), ], }) - .compileComponents() - .then(() => { - someComponentFixture = TestBed.createComponent(SomeComponent); - someComponent = someComponentFixture.componentInstance; - }); + .compileComponents(); + + someComponentFixture = TestBed.createComponent(SomeComponent); + someComponent = someComponentFixture.componentInstance; }); afterEach(() => { @@ -60,24 +58,25 @@ Some guidelines: describe('ParticipationSubmissionComponent', () => { ... - beforeEach(() => { - return TestBed.configureTestingModule({ - imports: [ArtemisTestModule, NgxDatatableModule, ArtemisResultModule, ArtemisSharedModule, TranslateModule.forRoot(), RouterTestingModule], - declarations: [ + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + ArtemisTestModule, + NgxDatatableModule, + ArtemisResultModule, + ArtemisSharedModule, + TranslateModule.forRoot(), ParticipationSubmissionComponent, MockComponent(UpdatingResultComponent), MockComponent(AssessmentDetailComponent), MockComponent(ComplaintsForTutorComponent), ], providers: [ - ... + provideRouter([]), ], }) .overrideModule(ArtemisTestModule, { set: { declarations: [], exports: [] } }) - .compileComponents() - .then(() => { - ... - }); + .compileComponents(); }); }); @@ -94,10 +93,12 @@ Some guidelines: describe('ParticipationSubmissionComponent', () => { ... - beforeEach(() => { - return TestBed.configureTestingModule({ - imports: [ArtemisTestModule, RouterTestingModule, NgxDatatableModule], - declarations: [ + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + ArtemisTestModule, + RouterTestingModule, + NgxDatatableModule, ParticipationSubmissionComponent, MockComponent(UpdatingResultComponent), MockComponent(AssessmentDetailComponent), @@ -110,13 +111,10 @@ Some guidelines: MockComponent(ResultComponent), ], providers: [ - ... + provideRouter([]), ], }) - .compileComponents() - .then(() => { - ... - }); + .compileComponents(); }); }); @@ -158,11 +156,16 @@ Some guidelines: .. code:: ts - import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + import { provideHttpClient } from '@angular/common/http'; + import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing'; describe('SomeComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], + imports: [...], + providers: [ + provideHttpClient(), + provideHttpClientTesting(), + ], }); ... @@ -221,21 +224,18 @@ Some guidelines: let someComponentFixture: ComponentFixture; let someComponent: SomeComponent; - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [], - declarations: [ - SomeComponent, - ], + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SomeComponent], providers: [ + ... ], }) .overrideTemplate(SomeComponent, '') // DO NOT DO THIS - .compileComponents() - .then(() => { - someComponentFixture = TestBed.createComponent(SomeComponent); - someComponent = someComponentFixture.componentInstance; - }); + .compileComponents(); + + someComponentFixture = TestBed.createComponent(SomeComponent); + someComponent = someComponentFixture.componentInstance; }); }); diff --git a/docs/dev/guidelines/server-tests.rst b/docs/dev/guidelines/server-tests.rst index 1e95860b8064..ef2b61d586a4 100644 --- a/docs/dev/guidelines/server-tests.rst +++ b/docs/dev/guidelines/server-tests.rst @@ -151,19 +151,19 @@ Follow these tips to write performant tests: * Limit object creation in tests and the test setup. -6. Avoid using @MockBean -========================= +6. Avoid using @MockitoBean +=========================== -Do not use the ``@SpyBean`` or ``@MockBean`` annotation unless absolutely necessary or possibly in an abstract Superclass. `Here `__ you can see why in more detail. -Whenever``@MockBean`` appears in a class, the application context cache gets marked as dirty, meaning the runner will clean the cache after finishing the test class. The application context is restarted, which leads to an additional server start with runtime overhead. +Do not use the ``@MockitoSpyBean`` or ``@MockitoBean`` annotation unless absolutely necessary or possibly in an abstract Superclass. `Here `__ you can see why in more detail. +Whenever``@MockitoBean`` appears in a class, the application context cache gets marked as dirty, meaning the runner will clean the cache after finishing the test class. The application context is restarted, which leads to an additional server start with runtime overhead. We want to keep the number of server starts minimal. -Below is an example of how to replace a ``@SpyBean``. To test an edge case where an ``IOException`` is thrown, we mocked the service method so it threw an Exception. +Below is an example of how to replace a ``@MockitoSpyBean``. To test an edge case where an ``IOException`` is thrown, we mocked the service method so it threw an Exception. .. code-block:: java class TestExport extends AbstractSpringIntegrationIndependentTest { - @SpyBean + @MockitoSpyBean private FileUploadSubmissionExportService fileUploadSubmissionExportService; @Test @@ -174,7 +174,7 @@ Below is an example of how to replace a ``@SpyBean``. To test an edge case where } } -To avoid new SpyBeans, we now use `static mocks `__. Upon examining the ``export()`` method, we find a ``File.newOutputStream(..)`` call. +To avoid new MockitoSpyBeans, we now use `static mocks `__. Upon examining the ``export()`` method, we find a ``File.newOutputStream(..)`` call. Now, instead of mocking the whole service, we can mock the static method: .. code-block:: java diff --git a/docs/user/adaptive-learning/adaptive-learning-instructor.rst b/docs/user/adaptive-learning/adaptive-learning-instructor.rst index 0f78a71e920d..524f4287d55a 100644 --- a/docs/user/adaptive-learning/adaptive-learning-instructor.rst +++ b/docs/user/adaptive-learning/adaptive-learning-instructor.rst @@ -152,8 +152,10 @@ Learning Paths Instructors can enable learning paths for their courses either by editing the course or on the dedicated learning path management page. This will generate individualized learning paths for all course participants. -Once the feature is enabled, instructors get access to each student's learning path. Instructors can search for students by login or name and view their respective learning path graph. - +Once the feature is enabled, instructors gain access to the Learning Paths Management page, where they can view an overview of the status of the learning paths feature. +For example, if competencies have not yet been created or relationships between them are missing, the State panel will notify instructors of these issues. +Instructors can also review the individual learning paths of students. The table on this page displays each student's login, name, and progress within their learning path. By clicking on a student's progress, the instructor can open the learning path graph, which illustrates the relationships between competencies and prerequisites and shows the student's mastery level for each. +At the bottom of the page, instructors can find generalized information about the learning paths of all students. This includes a graph that presents the average mastery level for each competency or prerequisite across the entire class. |instructors-learning-path-management| .. |instructor-competency-management| image:: instructor/manage-competencies.png diff --git a/docs/user/adaptive-learning/adaptive-learning-student.rst b/docs/user/adaptive-learning/adaptive-learning-student.rst index e47c1965fb07..dd8a3e8e723d 100644 --- a/docs/user/adaptive-learning/adaptive-learning-student.rst +++ b/docs/user/adaptive-learning/adaptive-learning-student.rst @@ -38,15 +38,14 @@ Learning Paths -------------- Students can access their learning path in the learning path tab. Here, they can access recommended lecture units and participate in exercises. -Recommendations (visualized on the left) are generated via an intelligent agent that accounts for multiple metrics, e.g. prior performance, confidence, relations, and due dates, to support students in their selection of learning resources. -Students can use the up and down buttons to navigate to the previous or next recommendation respectively. Hovering over a node in the list will display more information about the learning resource. +Recommendations are generated via an intelligent agent that accounts for multiple metrics, e.g. prior performance, confidence, relations, and due dates, to support students in their selection of learning resources. +Students can use the "Previous" and "Next" buttons to navigate to the previous or next recommendation respectively. |students-learning-path-participation| -Students can access their learning path graph via the eye icon on the top left. The graph displays all competencies, lecture units, exercises, and their relations. Each competency consists of a start node, visualized by the competency rings displaying progress, confidence, and overall mastery, and an end node represented by a checkered flag. Edges link learning resources to a competency via the respective start and end nodes. If the resource is still pending, it displays as a play symbol. Upon completion of the task, it appears as a checkmark. -Users can read the graph from top to bottom, starting with the competencies that have no prerequisites, continuing downwards toward competencies that build upon prior knowledge. Students can zoom, pan, and drag the graph to navigate. For better orientation, the top right corner contains a mini-map. -On the bottom right of the graph, users can view a legend describing the different types of nodes. -Hovering over any node, e.g. exercise or competency, opens a popover containing essential information about the item, e.g. the type of exercise and title, or for competencies, the details, including the description. +Students can access all scheduled competencies and prerequisites by clicking on the title of the learning object they are currently viewing. Expanding a competency or prerequisite in the list reveals its associated learning objects, each indicating whether it has been completed. +To navigate to a specific learning object, students can simply click on its title. +For a broader view of how competencies and prerequisites are interconnected, students can open the course competency graph. This graph starts with competencies that have no prerequisites and progresses to those that build upon earlier knowledge. To aid navigation, a mini-map is available in the top-right corner. |students-learning-path-graph| diff --git a/docs/user/adaptive-learning/instructor/learning-path-management.png b/docs/user/adaptive-learning/instructor/learning-path-management.png index 871203c7d672..df2c773bd862 100644 Binary files a/docs/user/adaptive-learning/instructor/learning-path-management.png and b/docs/user/adaptive-learning/instructor/learning-path-management.png differ diff --git a/docs/user/adaptive-learning/student/students-learning-path-graph.png b/docs/user/adaptive-learning/student/students-learning-path-graph.png index 06d67eb408be..c3e9bf768db2 100644 Binary files a/docs/user/adaptive-learning/student/students-learning-path-graph.png and b/docs/user/adaptive-learning/student/students-learning-path-graph.png differ diff --git a/docs/user/adaptive-learning/student/students-learning-path-participation.png b/docs/user/adaptive-learning/student/students-learning-path-participation.png index 47c29350c95d..8a1490409ead 100644 Binary files a/docs/user/adaptive-learning/student/students-learning-path-participation.png and b/docs/user/adaptive-learning/student/students-learning-path-participation.png differ diff --git a/docs/user/exercises/programming-exercise-setup.inc b/docs/user/exercises/programming-exercise-setup.inc index 8a2dc7cf9f78..563f65f48361 100644 --- a/docs/user/exercises/programming-exercise-setup.inc +++ b/docs/user/exercises/programming-exercise-setup.inc @@ -404,7 +404,8 @@ Edit Maximum Build Duration ^^^^^^^^^^^^^^^^^^^^^^^^^^^ **This option is only available when using** :ref:`integrated code lifecycle` -This section is optional. In most cases, the preconfigured build script does not need to be changed. + +This section is optional. In most cases, the default maximum build duration does not need to be changed. The maximum build duration is the time limit for the build plan to execute. If the build plan exceeds this time limit, it will be terminated. The default value is 120 seconds. You can change the maximum build duration by using the slider. @@ -412,6 +413,29 @@ You can change the maximum build duration by using the slider. .. figure:: programming/timeout-slider.png :align: center +Edit Container Configuration +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**This option is only available when using** :ref:`integrated code lifecycle` + +This section is optional. In most cases, the default container configuration does not need to be changed. + +Currently, instructors can only change whether the container has internet access and add additional environment variables. +Disabling internet access can be useful if instructors want to prevent students from downloading additional dependencies during the build process. +If internet access is disabled, the container cannot access the internet during the build process. Thus, it will not be able to download additional dependencies. +The dependencies must then be included/cached in the docker image. + +Additional environment variables can be added to the container configuration. This can be useful if the build process requires additional environment variables to be set. + +.. figure:: programming/docker-flags-edit.png + :align: center + +We plan to add more options to the container configuration in the future. + +.. warning:: + - Disabling internet access is not currently supported for Swift and Haskell exercises. + + .. _configure_static_code_analysis_tools: Configure static code analysis diff --git a/docs/user/exercises/programming/docker-flags-edit.png b/docs/user/exercises/programming/docker-flags-edit.png new file mode 100644 index 000000000000..06a030f69f18 Binary files /dev/null and b/docs/user/exercises/programming/docker-flags-edit.png differ diff --git a/gradle.properties b/gradle.properties index b234044bcc8f..f2a82add9928 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,25 +7,27 @@ npm_version=10.8.0 # Dependency versions jhipster_dependencies_version=8.7.2 -spring_boot_version=3.3.6 -spring_security_version=6.3.5 -# TODO: upgrading to 6.6.0 currently leads to issues due to internal changes in Hibernate and potentially wrong use in Artemis server code +spring_boot_version=3.4.0 +spring_framework_version=6.2.0 +spring_cloud_version=4.2.0 +spring_security_version=6.4.1 +# TODO: upgrading to 6.6.x currently leads to issues due to internal changes in Hibernate and potentially wrong use in Artemis server code hibernate_version=6.4.10.Final # TODO: can we update to 5.x? opensaml_version=4.3.2 jwt_version=0.12.6 jaxb_runtime_version=4.0.5 hazelcast_version=5.5.0 -fasterxml_version=2.18.1 -jgit_version=7.0.0.202409031743-r +fasterxml_version=2.18.2 +jgit_version=7.1.0.202411261347-r sshd_version=2.14.0 -checkstyle_version=10.20.1 +checkstyle_version=10.20.2 jplag_version=5.1.0 -# not really used in Artemis, nor Jplag, nor the used version of Stanford CoreNLP, but we use the latest to avoid security vulnerabilities -# NOTE: we do not need to use the latest version 9.x here as long as Stanford CoreNLP does not reference it +# not really used in Artemis, nor JPlag, nor the used version of Stanford CoreNLP, but we use the latest to avoid security vulnerability warnings +# NOTE: we cannot need to use the latest version 9.x or 10.x here as long as Stanford CoreNLP does not reference it lucene_version=8.11.4 slf4j_version=2.0.16 -sentry_version=7.18.0 +sentry_version=7.18.1 liquibase_version=4.30.0 docker_java_version=3.4.0 logback_version=1.5.12 @@ -47,7 +49,7 @@ apt_plugin_version=0.21 liquibase_plugin_version=2.1.1 modernizer_plugin_version=1.10.0 -org.gradle.jvmargs=-Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -Duser.country=US -Duser.language=en \ +org.gradle.jvmargs=-Xmx2g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -Duser.country=US -Duser.language=en \ --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \ diff --git a/jest.config.js b/jest.config.js index 96eeb24f0890..8f3838cd5088 100644 --- a/jest.config.js +++ b/jest.config.js @@ -105,10 +105,10 @@ module.exports = { coverageThreshold: { global: { // TODO: in the future, the following values should increase to at least 90% - statements: 87.66, + statements: 87.69, branches: 73.79, - functions: 82.17, - lines: 87.72, + functions: 82.27, + lines: 87.74, }, }, coverageReporters: ['clover', 'json', 'lcov', 'text-summary'], diff --git a/package-lock.json b/package-lock.json index 65186daa3bc0..18805ac78ad9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,27 +1,27 @@ { "name": "artemis", - "version": "7.7.4", + "version": "7.7.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "artemis", - "version": "7.7.4", + "version": "7.7.5", "hasInstallScript": true, "license": "MIT", "dependencies": { - "@angular/animations": "18.2.12", + "@angular/animations": "18.2.13", "@angular/cdk": "18.2.13", - "@angular/common": "18.2.12", - "@angular/compiler": "18.2.12", - "@angular/core": "18.2.12", - "@angular/forms": "18.2.12", - "@angular/localize": "18.2.12", + "@angular/common": "18.2.13", + "@angular/compiler": "18.2.13", + "@angular/core": "18.2.13", + "@angular/forms": "18.2.13", + "@angular/localize": "18.2.13", "@angular/material": "18.2.13", - "@angular/platform-browser": "18.2.12", - "@angular/platform-browser-dynamic": "18.2.12", - "@angular/router": "18.2.12", - "@angular/service-worker": "18.2.12", + "@angular/platform-browser": "18.2.13", + "@angular/platform-browser-dynamic": "18.2.13", + "@angular/router": "18.2.13", + "@angular/service-worker": "18.2.13", "@ctrl/ngx-emoji-mart": "9.2.0", "@danielmoncada/angular-datetime-picker": "18.1.0", "@fingerprintjs/fingerprintjs": "4.5.1", @@ -33,7 +33,7 @@ "@ng-bootstrap/ng-bootstrap": "17.0.1", "@ngx-translate/core": "16.0.3", "@ngx-translate/http-loader": "16.0.0", - "@sentry/angular": "8.39.0", + "@sentry/angular": "8.42.0", "@siemens/ngx-datatable": "22.4.1", "@swimlane/ngx-charts": "21.0.0", "@swimlane/ngx-graph": "8.4.0", @@ -45,7 +45,7 @@ "crypto-js": "4.2.0", "dayjs": "1.11.13", "diff-match-patch-typescript": "1.1.0", - "dompurify": "3.2.1", + "dompurify": "3.2.2", "emoji-js": "3.8.0", "export-to-csv": "1.4.0", "fast-json-patch": "3.1.1", @@ -65,7 +65,7 @@ "ngx-webstorage": "18.0.0", "papaparse": "5.4.1", "pdf-lib": "1.17.1", - "pdfjs-dist": "4.8.69", + "pdfjs-dist": "4.9.155", "rxjs": "7.8.1", "simple-statistics": "7.8.7", "smoothscroll-polyfill": "0.4.4", @@ -88,9 +88,9 @@ "@angular-eslint/schematics": "18.4.1", "@angular-eslint/template-parser": "18.4.1", "@angular/cli": "18.2.12", - "@angular/compiler-cli": "18.2.12", - "@angular/language-service": "18.2.12", - "@sentry/types": "8.39.0", + "@angular/compiler-cli": "18.2.13", + "@angular/language-service": "18.2.13", + "@sentry/types": "8.42.0", "@types/crypto-js": "4.2.2", "@types/d3-shape": "3.1.6", "@types/dompurify": "3.0.5", @@ -98,15 +98,15 @@ "@types/jest": "29.5.14", "@types/lodash-es": "4.17.12", "@types/markdown-it": "14.1.2", - "@types/node": "22.9.1", + "@types/node": "22.10.1", "@types/papaparse": "5.3.15", "@types/smoothscroll-polyfill": "0.3.4", "@types/sockjs-client": "1.5.4", "@types/turndown": "5.0.5", "@types/uuid": "10.0.0", - "@typescript-eslint/eslint-plugin": "8.15.0", - "@typescript-eslint/parser": "8.15.0", - "eslint": "9.15.0", + "@typescript-eslint/eslint-plugin": "8.17.0", + "@typescript-eslint/parser": "8.17.0", + "eslint": "9.16.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-deprecation": "3.0.0", "eslint-plugin-jest": "28.9.0", @@ -120,13 +120,13 @@ "jest-extended": "4.0.2", "jest-fail-on-console": "3.3.1", "jest-junit": "16.0.0", - "jest-preset-angular": "14.3.2", + "jest-preset-angular": "14.4.2", "lint-staged": "15.2.10", "ng-mocks": "14.13.1", "ngxtension": "4.1.0", - "prettier": "3.3.3", + "prettier": "3.4.2", "rimraf": "6.0.1", - "sass": "1.81.0", + "sass": "1.82.0", "ts-jest": "29.2.5", "typescript": "5.5.4", "weak-napi": "2.0.2" @@ -531,16 +531,6 @@ "@angular-devkit/schematics": ">= 18.0.0 < 19.0.0" } }, - "node_modules/@angular-eslint/schematics/node_modules/ignore": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz", - "integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/@angular-eslint/template-parser": { "version": "18.4.1", "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-18.4.1.tgz", @@ -572,9 +562,9 @@ } }, "node_modules/@angular/animations": { - "version": "18.2.12", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.12.tgz", - "integrity": "sha512-XcWH/VFQ1Rddhdqi/iU8lW3Qg96yVx1NPfrO5lhcSSvVUzYWTZ5r+jh3GqYqUgPWyEp1Kpw3FLsOgVcGcBWQkQ==", + "version": "18.2.13", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.13.tgz", + "integrity": "sha512-rG5J5Ek5Hg+Tz2NjkNOaG6PupiNK/lPfophXpsR1t/nWujqnMWX2krahD/i6kgD+jNWNKCJCYSOVvCx/BHOtKA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -583,7 +573,7 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.12" + "@angular/core": "18.2.13" } }, "node_modules/@angular/build": { @@ -732,9 +722,9 @@ } }, "node_modules/@angular/common": { - "version": "18.2.12", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.12.tgz", - "integrity": "sha512-gI5o8Bccsi8ow8Wk2vG4Tw/Rw9LoHEA9j8+qHKNR/55SCBsz68Syg310dSyxy+sApJO2WiqIadr5VP36dlSUFw==", + "version": "18.2.13", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.13.tgz", + "integrity": "sha512-4ZqrNp1PoZo7VNvW+sbSc2CB2axP1sCH2wXl8B0wdjsj8JY1hF1OhuugwhpAHtGxqewed2kCXayE+ZJqSTV4jw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -743,14 +733,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.12", + "@angular/core": "18.2.13", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "18.2.12", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.12.tgz", - "integrity": "sha512-D5d5dLrjQal5DbAXJJNSsCC3UxzjOI2wbc+Iv+LOpRM1gpNwuYfZMX5W7cj62Ce4G2++78CJSppdKBp8D4HErQ==", + "version": "18.2.13", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.13.tgz", + "integrity": "sha512-TzWcrkopyjFF+WeDr2cRe8CcHjU72KfYV3Sm2TkBkcXrkYX5sDjGWrBGrG3hRB4e4okqchrOCvm1MiTdy2vKMA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -759,7 +749,7 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.12" + "@angular/core": "18.2.13" }, "peerDependenciesMeta": { "@angular/core": { @@ -768,9 +758,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "18.2.12", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.12.tgz", - "integrity": "sha512-IWimTNq5Q+i2Wxev6HLqnN4iYbPvLz04W1BBycT1LfGUsHcjFYLuUqbeUzHbk2snmBAzXkixgVpo8SF6P4Y5Pg==", + "version": "18.2.13", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.13.tgz", + "integrity": "sha512-DBSh4AQwkiJDSiVvJATRmjxf6wyUs9pwQLgaFdSlfuTRO+sdb0J2z1r3BYm8t0IqdoyXzdZq2YCH43EmyvD71g==", "license": "MIT", "dependencies": { "@babel/core": "7.25.2", @@ -791,7 +781,7 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "18.2.12", + "@angular/compiler": "18.2.13", "typescript": ">=5.4 <5.6" } }, @@ -824,9 +814,9 @@ } }, "node_modules/@angular/core": { - "version": "18.2.12", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.12.tgz", - "integrity": "sha512-wCf/OObwS6bpM60rk6bpMpCRGp0DlMLB1WNAMtfcaPNyqimVV5Bm98mWRhkOuRyvU3fU7iHhM/10ePVaoyu9+A==", + "version": "18.2.13", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.13.tgz", + "integrity": "sha512-8mbWHMgO95OuFV1Ejy4oKmbe9NOJ3WazQf/f7wks8Bck7pcihd0IKhlPBNjFllbF5o+04EYSwFhEtvEgjMDClA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -840,9 +830,9 @@ } }, "node_modules/@angular/forms": { - "version": "18.2.12", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.12.tgz", - "integrity": "sha512-FsukBJEU6jfAmht7TrODTkct/o4iwCZvGozuThOp0tYUPD/E1rZZzuKjEyTnT5Azpfkf0Wqx1nmpz80cczELOQ==", + "version": "18.2.13", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.13.tgz", + "integrity": "sha512-A67D867fu3DSBhdLWWZl/F5pr7v2+dRM2u3U7ZJ0ewh4a+sv+0yqWdJW+a8xIoiHxS+btGEJL2qAKJiH+MCFfg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -851,16 +841,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.12", - "@angular/core": "18.2.12", - "@angular/platform-browser": "18.2.12", + "@angular/common": "18.2.13", + "@angular/core": "18.2.13", + "@angular/platform-browser": "18.2.13", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/language-service": { - "version": "18.2.12", - "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-18.2.12.tgz", - "integrity": "sha512-oaiVAnGzmPZvrXdGh8XnosaqfEPbZxO2225MxbbrD49XTqUgpaS2zrz1Uf5j42e8qytA2kj8tckLq7PAMm0D1w==", + "version": "18.2.13", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-18.2.13.tgz", + "integrity": "sha512-4E4VJDrbOAxS69F9C1twQPbR9AjY47Qlz8+lwg5lJOyUJ4GoEThLbXKfadt/vIeYBwMJ7fIsYWXD0Dlmxh4k+w==", "dev": true, "license": "MIT", "engines": { @@ -868,9 +858,9 @@ } }, "node_modules/@angular/localize": { - "version": "18.2.12", - "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-18.2.12.tgz", - "integrity": "sha512-qC3cYFh3miR9revmHGlfbGvugcsK6nQud4QKBNyTUp1XZRrEE0yzPvvsnmbv2lHUOazrvTxQpfVZZKpiifgoLw==", + "version": "18.2.13", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-18.2.13.tgz", + "integrity": "sha512-qQaIYdDS/l1w6tr/wpOoimjpmoJU0WmB8AGbNeKLoM36K+ix6hkvn67+UgkpZtaDHZylm8GsGW1NjzpM2tr3pA==", "license": "MIT", "dependencies": { "@babel/core": "7.25.2", @@ -887,8 +877,8 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "18.2.12", - "@angular/compiler-cli": "18.2.12" + "@angular/compiler": "18.2.13", + "@angular/compiler-cli": "18.2.13" } }, "node_modules/@angular/material": { @@ -910,9 +900,9 @@ } }, "node_modules/@angular/platform-browser": { - "version": "18.2.12", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.12.tgz", - "integrity": "sha512-DRSMznuxuecrs+v5BRyd60/R4vjkQtuYUEPfzdo+rqxM83Dmr3PGtnqPRgd5oAFUbATxf02hQXijRD27K7rZRg==", + "version": "18.2.13", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.13.tgz", + "integrity": "sha512-tu7ZzY6qD3ATdWFzcTcsAKe7M6cJeWbT/4/bF9unyGO3XBPcNYDKoiz10+7ap2PUd0fmPwvuvTvSNJiFEBnB8Q==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -921,9 +911,9 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/animations": "18.2.12", - "@angular/common": "18.2.12", - "@angular/core": "18.2.12" + "@angular/animations": "18.2.13", + "@angular/common": "18.2.13", + "@angular/core": "18.2.13" }, "peerDependenciesMeta": { "@angular/animations": { @@ -932,9 +922,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "18.2.12", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.12.tgz", - "integrity": "sha512-dv1QEjYpcFno6+oUeGEDRWpB5g2Ufb0XkUbLJQIgrOk1Qbyzb8tmpDpTjok8jcKdquigMRWolr6Y1EOicfRlLw==", + "version": "18.2.13", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.13.tgz", + "integrity": "sha512-kbQCf9+8EpuJC7buBxhSiwBtXvjAwAKh6MznD6zd2pyCYqfY6gfRCZQRtK59IfgVtKmEONWI9grEyNIRoTmqJg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -943,16 +933,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.12", - "@angular/compiler": "18.2.12", - "@angular/core": "18.2.12", - "@angular/platform-browser": "18.2.12" + "@angular/common": "18.2.13", + "@angular/compiler": "18.2.13", + "@angular/core": "18.2.13", + "@angular/platform-browser": "18.2.13" } }, "node_modules/@angular/router": { - "version": "18.2.12", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.12.tgz", - "integrity": "sha512-cz/1YWOZadAT35PPPYmpK3HSzKOE56nlUHue5bFkw73VSZr2iBn03ALLpd9YKzWgRmx3y7DqnlQtCkDu9JPGKQ==", + "version": "18.2.13", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.13.tgz", + "integrity": "sha512-VKmfgi/r/CkyBq9nChQ/ptmfu0JT/8ONnLVJ5H+SkFLRYJcIRyHLKjRihMCyVm6xM5yktOdCaW73NTQrFz7+bg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -961,16 +951,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.12", - "@angular/core": "18.2.12", - "@angular/platform-browser": "18.2.12", + "@angular/common": "18.2.13", + "@angular/core": "18.2.13", + "@angular/platform-browser": "18.2.13", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/service-worker": { - "version": "18.2.12", - "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-18.2.12.tgz", - "integrity": "sha512-rgztA+Eduo69y6cvSDtAXC5lMTWjgowSSreiyM4ssyjwd8vD6h2TZp/3slr8Tt6+Lh9J4bK+UdcqMIjIdDxwSw==", + "version": "18.2.13", + "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-18.2.13.tgz", + "integrity": "sha512-fVC943qEqGNUy923NMmSSzfoIqNw2k2UbG/3Y4QEmel/nZFWHA3PhiYr+lE7J3RhRHFMmnNP1bmXDJgy+R+pzA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -982,8 +972,8 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.12", - "@angular/core": "18.2.12" + "@angular/common": "18.2.13", + "@angular/core": "18.2.13" } }, "node_modules/@babel/code-frame": { @@ -3549,6 +3539,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -3570,9 +3570,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.15.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.15.0.tgz", - "integrity": "sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==", + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz", + "integrity": "sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==", "dev": true, "license": "MIT", "engines": { @@ -4828,105 +4828,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "peer": true, - "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@mixmark-io/domino": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz", @@ -5017,6 +4918,188 @@ "win32" ] }, + "node_modules/@napi-rs/canvas": { + "version": "0.1.65", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.65.tgz", + "integrity": "sha512-YcFhXQcp+b2d38zFOJNbpyPHnIL7KAEkhJQ+UeeKI5IpE9B8Cpf/M6RiHPQXSsSqnYbrfFylnW49dyh2oeSblQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.65", + "@napi-rs/canvas-darwin-arm64": "0.1.65", + "@napi-rs/canvas-darwin-x64": "0.1.65", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.65", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.65", + "@napi-rs/canvas-linux-arm64-musl": "0.1.65", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.65", + "@napi-rs/canvas-linux-x64-gnu": "0.1.65", + "@napi-rs/canvas-linux-x64-musl": "0.1.65", + "@napi-rs/canvas-win32-x64-msvc": "0.1.65" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.65", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.65.tgz", + "integrity": "sha512-ZYwqFYEKcT5Zr8lbiaJNJj/poLaeK2TncolY914r+gD2TJNeP7ZqvE7A2SX/1C9MB4E3DQEwm3YhL3WEf0x3MQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.65", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.65.tgz", + "integrity": "sha512-Pg1pfiJEyDIsX+V0QaJPRWvXbw5zmWAk3bivFCvt/5pwZb37/sT6E/RqPHT9NnqpDyKW6SriwY9ypjljysUA1Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.65", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.65.tgz", + "integrity": "sha512-3Tr+/HjdJN7Z/VKIcsxV2DvDIibZCExgfYTgljCkUSFuoI7iNkOE6Dc1Q6j212EB9PeO8KmfrViBqHYT6IwWkA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.65", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.65.tgz", + "integrity": "sha512-3KP+dYObH7CVkZMZWwk1WX9jRjL+EKdQtD43H8MOI+illf+dwqLlecdQ4d9bQRIxELKJ8dyPWY4fOp/Ngufrdg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.65", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.65.tgz", + "integrity": "sha512-Ka3StKz7Dq7kjTF3nNJCq43UN/VlANS7qGE3dWkn1d+tQNsCRy/wRmyt1TUFzIjRqcTFMQNRbgYq84+53UBA0A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.65", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.65.tgz", + "integrity": "sha512-O4xMASm2JrmqYoiDyxVWi+z5C14H+oVEag2rZ5iIA67dhWqYZB+iO7wCFpBYRj31JPBR29FOsu6X9zL+DwBFdw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.65", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.65.tgz", + "integrity": "sha512-dblWDaA59ZU8bPbkfM+riSke7sFbNZ70LEevUdI5rgiFEUzYUQlU34gSBzemTACj5rCWt1BYeu0GfkLSjNMBSw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.65", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.65.tgz", + "integrity": "sha512-wsp+atutw13OJXGU3DDkdngtBDoEg01IuK5xMe0L6VFPV8maGkh17CXze078OD5QJOc6kFyw3DDscMLOPF8+oA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.65", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.65.tgz", + "integrity": "sha512-odX+nN+IozWzhdj31INcHz3Iy9+EckNw+VqsZcaUxZOTu7/3FmktRNI6aC1qe5minZNv1m05YOS1FVf7fvmjlA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.65", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.65.tgz", + "integrity": "sha512-RZQX3luWnlNWgdMnLMQ1hyfQraeAn9lnxWWVCHuUM4tAWEV8UDdeb7cMwmJW7eyt8kAosmjeHt3cylQMHOxGFg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.4.tgz", @@ -5376,9 +5459,9 @@ } }, "node_modules/@nx/devkit": { - "version": "20.1.1", - "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-20.1.1.tgz", - "integrity": "sha512-sqihJhJQERCTl0KmKmpRFxWxuTnH8yRqdo8T5uGGaHzTNiMdIp5smTF2dBs7/OMkZDxcJc4dKvcFWfreZr8XNw==", + "version": "20.1.4", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-20.1.4.tgz", + "integrity": "sha512-Opz7eRPmpt3e4SGkbwZbE9Bg3MhKeivh1QTNCj4tQVAB4gucz0lW/F3mdtRDFdj6gUbqIc5rRrbO/DGlNaEzYw==", "dev": true, "license": "MIT", "dependencies": { @@ -5395,6 +5478,16 @@ "nx": ">= 19 <= 21" } }, + "node_modules/@nx/devkit/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/@nx/devkit/node_modules/minimatch": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", @@ -5422,9 +5515,9 @@ } }, "node_modules/@nx/nx-darwin-arm64": { - "version": "20.1.1", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-20.1.1.tgz", - "integrity": "sha512-Ah0ShPQaMfvzVfhsyuI6hNB0bmwLHJqqrWldZeF97SFPhv6vfKdcdlZmSnask+V4N5z9TOCUmCMu2asMQa7+kw==", + "version": "20.1.4", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-20.1.4.tgz", + "integrity": "sha512-afyDOZbIyHi6BgKk+Bb4RI1t8dZ6/oIbOY89z4mBPNNevZkbGqUfMwO2vjKnaOoThcjT93SEMJfCLGL8i857ww==", "cpu": [ "arm64" ], @@ -5439,9 +5532,9 @@ } }, "node_modules/@nx/nx-darwin-x64": { - "version": "20.1.1", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-20.1.1.tgz", - "integrity": "sha512-TmdX6pbzclvPGsttTTaZhdF46HV1vfvYSHJaSMsYJX68l3gcQnAJ1ZRDksEgkYeAy+O9KrPimD84NM5W/JvqcQ==", + "version": "20.1.4", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-20.1.4.tgz", + "integrity": "sha512-aiYklAt95aX0EinepJRryMna8K53G52ngYOFuac1G8iLlguinJvg/YgSKCf7GOAzec8b7Hm7KauPjSJE/P3/iw==", "cpu": [ "x64" ], @@ -5456,9 +5549,9 @@ } }, "node_modules/@nx/nx-freebsd-x64": { - "version": "20.1.1", - "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-20.1.1.tgz", - "integrity": "sha512-7/7f3GbUbdvtTFOb/8wcaSQYkhVIxcC4UzFJM5yEyXPJmIrglk+RX3SLuOFRBFJnO+Z7D6jLUnLOBHKCGfqLVw==", + "version": "20.1.4", + "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-20.1.4.tgz", + "integrity": "sha512-WUh4bsLK+e7wuN3lE3ZQUj+xQKdWU4P4RymutfLQQnPYiilCMtFwITcvDmazmOHFWI2vPhzSyYJRbOu+YMIR3A==", "cpu": [ "x64" ], @@ -5473,9 +5566,9 @@ } }, "node_modules/@nx/nx-linux-arm-gnueabihf": { - "version": "20.1.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-20.1.1.tgz", - "integrity": "sha512-VxpMz5jCZ5gnk1gP2jDBCheYs7qOwQoJmzGbEB8hNy0CwRH/G8pL4RRo4Sz+4aiF6Z+9eax5RM2/Syh+bS0uJw==", + "version": "20.1.4", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-20.1.4.tgz", + "integrity": "sha512-9vPMw5s89v3od7aw3enTWjdMSCAmQ0tIA89Uz7xbbjB2kX2mAdihSzAKd9woi/cj+ROnY+ynNXzU9UjqhfxdBg==", "cpu": [ "arm" ], @@ -5490,9 +5583,9 @@ } }, "node_modules/@nx/nx-linux-arm64-gnu": { - "version": "20.1.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-20.1.1.tgz", - "integrity": "sha512-8T2+j4KvsWb6ljW1Y2s/uCSt4Drtlsr3GSrGdvcETW0IKaTfKZAJlxTLAWQHEF88hP6GAJRGxNrgmUHMr8HwUA==", + "version": "20.1.4", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-20.1.4.tgz", + "integrity": "sha512-JUE4l8utr9KmQSG9tO2Qw5R5i/bZ16s1+J5xnEar7UfcSOfOLqxGHS7HCBUZcfr46dmtv6KjIC83uHMs19AwDQ==", "cpu": [ "arm64" ], @@ -5507,9 +5600,9 @@ } }, "node_modules/@nx/nx-linux-arm64-musl": { - "version": "20.1.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-20.1.1.tgz", - "integrity": "sha512-TI964w+HFUqG6elriKwQPRX7QRxVRMz5YKdNPgf4+ab4epQ379kwJQEHlyOHR72ir8Tl46z3BoPjvmaLylrT4Q==", + "version": "20.1.4", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-20.1.4.tgz", + "integrity": "sha512-EaPUDqXvnPc/ure0x7N+5lRYvk5zqOQ3LzFOTRPWdqnFXejyTkGjZEYWbLFIJTFrvyEdpfaPTHyNmCHUrEz9TQ==", "cpu": [ "arm64" ], @@ -5524,9 +5617,9 @@ } }, "node_modules/@nx/nx-linux-x64-gnu": { - "version": "20.1.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-20.1.1.tgz", - "integrity": "sha512-Sg2tQ0v3KP9cAqQST16YR+dT/NbirPts6by+A4vhOtaBrZFVqm9P89K9UdcJf4Aj1CaGbs84lotp2aM4E4bQPA==", + "version": "20.1.4", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-20.1.4.tgz", + "integrity": "sha512-vaWV37ZayfyckVI/faWdQWIV9XQb06ZT8jHQnwgSd9tKbGz37vN30eYtgZlFL0P4bHfhjtmMXnLvADmfyO/KOw==", "cpu": [ "x64" ], @@ -5541,9 +5634,9 @@ } }, "node_modules/@nx/nx-linux-x64-musl": { - "version": "20.1.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-20.1.1.tgz", - "integrity": "sha512-ekKvuIMRJRhZnkWIWEr4TRVEAyKVDgEMwqk83ilB0Mqpj2RoOKbw7jZFvWcxJWI4kSeZjTea3xCWGNPa1GfCww==", + "version": "20.1.4", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-20.1.4.tgz", + "integrity": "sha512-wjq4Ea1oweBsIA9jq+jDT6BALxv/uac0aFykwoN23dOiwwSMFWMxbXUuBrxp0LjMFGV49S62kVDoRezukvkiZA==", "cpu": [ "x64" ], @@ -5558,9 +5651,9 @@ } }, "node_modules/@nx/nx-win32-arm64-msvc": { - "version": "20.1.1", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-20.1.1.tgz", - "integrity": "sha512-JRycFkk6U8A1sXaDmSFA2HMKT2js3HK/+nI+auyITRqVbV79/r6ir/oFSgIjKth8j/vVbGDL8I4E3nEQ7leZYw==", + "version": "20.1.4", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-20.1.4.tgz", + "integrity": "sha512-d9jN8biyEJh4Mjdc3RU1j/+WIOjrO9mCDxYuERXP2ELaNsOk0tJgcXE1xsa9AF88AHGpOkCOS2rxy61DKBtFKg==", "cpu": [ "arm64" ], @@ -5575,9 +5668,9 @@ } }, "node_modules/@nx/nx-win32-x64-msvc": { - "version": "20.1.1", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-20.1.1.tgz", - "integrity": "sha512-VwxmJU7o8KqTZ+KYk7atoWOUykKd8D4hdgKqqltdq/UBfsAWD/JCFt5OB/VFvrGDbK6I6iKpMvXWlHy4gkXQiw==", + "version": "20.1.4", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-20.1.4.tgz", + "integrity": "sha512-s3RwOkkWKzOflbTmc5MRc4EH2mk1AkJ/V8Gu3Qi2QncF9r1GrR7hDxROpu0MEoHfIhRG+d+n8OGX31nC9GZWUg==", "cpu": [ "x64" ], @@ -6274,132 +6367,108 @@ } }, "node_modules/@sentry-internal/browser-utils": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.39.0.tgz", - "integrity": "sha512-5jcO3os1aQIMNZptniMUCCkZ3KOvyUPSyrQeGB7NxhJoieIwmopo5qIXyeRLHu0htL7H7A1gPYln6Ji3d/KUUA==", + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.42.0.tgz", + "integrity": "sha512-xzgRI0wglKYsPrna574w1t38aftuvo44gjOKFvPNGPnYfiW9y4m+64kUz3JFbtanvOrKPcaITpdYiB4DeJXEbA==", "license": "MIT", "dependencies": { - "@sentry/core": "8.39.0", - "@sentry/types": "8.39.0", - "@sentry/utils": "8.39.0" + "@sentry/core": "8.42.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/feedback": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.39.0.tgz", - "integrity": "sha512-V5J/tnzAK8bXdXQzY7lnlYMqfTKgI+9BD7L7oHxQnDUzlShsV14xFGZVhEbPsjYficdIN9wpoYIyWDxwrFX1Qg==", + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.42.0.tgz", + "integrity": "sha512-dkIw5Wdukwzngg5gNJ0QcK48LyJaMAnBspqTqZ3ItR01STi6Z+6+/Bt5XgmrvDgRD+FNBinflc5zMmfdFXXhvw==", "license": "MIT", "dependencies": { - "@sentry/core": "8.39.0", - "@sentry/types": "8.39.0", - "@sentry/utils": "8.39.0" + "@sentry/core": "8.42.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/replay": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.39.0.tgz", - "integrity": "sha512-1IEXhg2XuKC1hx/Pf5p2L7McKjQPfVOWyQhjNUH2mHWbpOyvc1BhZoZKCgbbspwOAVuvj4n40PvOVyjfzU5Yew==", + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.42.0.tgz", + "integrity": "sha512-oNcJEBlDfXnRFYC5Mxj5fairyZHNqlnU4g8kPuztB9G5zlsyLgWfPxzcn1ixVQunth2/WZRklDi4o1ZfyHww7w==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "8.39.0", - "@sentry/core": "8.39.0", - "@sentry/types": "8.39.0", - "@sentry/utils": "8.39.0" + "@sentry-internal/browser-utils": "8.42.0", + "@sentry/core": "8.42.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/replay-canvas": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.39.0.tgz", - "integrity": "sha512-NCp4E60SFfg9pXdMgcdpctYENFOvJ58UPGllGjO3xpYoMkd4DGZQp947Tgw9hATTCDnyYNIy5v/zYbDV4Wbw3w==", + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.42.0.tgz", + "integrity": "sha512-XrPErqVhPsPh/oFLVKvz7Wb+Fi2J1zCPLeZCxWqFuPWI2agRyLVu0KvqJyzSpSrRAEJC/XFzuSVILlYlXXSfgA==", "license": "MIT", "dependencies": { - "@sentry-internal/replay": "8.39.0", - "@sentry/core": "8.39.0", - "@sentry/types": "8.39.0", - "@sentry/utils": "8.39.0" + "@sentry-internal/replay": "8.42.0", + "@sentry/core": "8.42.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry/angular": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@sentry/angular/-/angular-8.39.0.tgz", - "integrity": "sha512-yke0NULFosz4Fap9NGKTVzRKoJRx8+sAC8jA2qdU49SUtxon+L3LN5D6QbE402kdMWEscxKa1cHrgfIvJfOZZA==", + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@sentry/angular/-/angular-8.42.0.tgz", + "integrity": "sha512-gQ3gHNw7FadlLEtE57l9AZ2bkW1bVAk8FnbOkpc3NXkBJTKtxWODbhqCGDxGOWplJGzVOJ4EmXU2GHm7APOdwA==", "license": "MIT", "dependencies": { - "@sentry/browser": "8.39.0", - "@sentry/core": "8.39.0", - "@sentry/types": "8.39.0", - "@sentry/utils": "8.39.0", + "@sentry/browser": "8.42.0", + "@sentry/core": "8.42.0", "tslib": "^2.4.1" }, "engines": { "node": ">=14.18" }, "peerDependencies": { - "@angular/common": ">= 14.x <= 18.x", - "@angular/core": ">= 14.x <= 18.x", - "@angular/router": ">= 14.x <= 18.x", + "@angular/common": ">= 14.x <= 19.x", + "@angular/core": ">= 14.x <= 19.x", + "@angular/router": ">= 14.x <= 19.x", "rxjs": "^6.5.5 || ^7.x" } }, "node_modules/@sentry/browser": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.39.0.tgz", - "integrity": "sha512-Xpqh84MnqoFID0owbugTeq/3QXgNwc3EdHAN/HFUdxEAyJS4j7Wi1DIBXN+ZRzMYX3m2QHOAymCWjnFtv+H8WQ==", + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.42.0.tgz", + "integrity": "sha512-lStrEk609KJHwXfDrOgoYVVoFFExixHywxSExk7ZDtwj2YPv6r6Y1gogvgr7dAZj7jWzadHkxZ33l9EOSJBfug==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "8.39.0", - "@sentry-internal/feedback": "8.39.0", - "@sentry-internal/replay": "8.39.0", - "@sentry-internal/replay-canvas": "8.39.0", - "@sentry/core": "8.39.0", - "@sentry/types": "8.39.0", - "@sentry/utils": "8.39.0" + "@sentry-internal/browser-utils": "8.42.0", + "@sentry-internal/feedback": "8.42.0", + "@sentry-internal/replay": "8.42.0", + "@sentry-internal/replay-canvas": "8.42.0", + "@sentry/core": "8.42.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry/core": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.39.0.tgz", - "integrity": "sha512-rg2mHtwdCaedqub7bd+ht08vZgtwPO7el5m5sPNeb7V75GcQwSziu6G02vGxCBCsAHpoFn1A+0JLEajaYzZI7w==", + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.42.0.tgz", + "integrity": "sha512-ac6O3pgoIbU6rpwz6LlwW0wp3/GAHuSI0C5IsTgIY6baN8rOBnlAtG6KrHDDkGmUQ2srxkDJu9n1O6Td3cBCqw==", "license": "MIT", - "dependencies": { - "@sentry/types": "8.39.0", - "@sentry/utils": "8.39.0" - }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry/types": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.39.0.tgz", - "integrity": "sha512-/n1bGkbJcSLZQpzd1Oksi8LFAMbcO8j/d+N8mcXS74GuhGgkxQiEwHF2CKTz6SHt8J4hrlyzqIwVzCevUOxZ2Q==", - "license": "MIT", - "engines": { - "node": ">=14.18" - } - }, - "node_modules/@sentry/utils": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.39.0.tgz", - "integrity": "sha512-pIBnr/cROds92CcYWBW3z1zFH4uJkMPL2AxEv/ZcLg/NTb1Okz/ZaDP+NMzUfzriYvFBOFk0wPk0h5sYx6Umqw==", + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.42.0.tgz", + "integrity": "sha512-oXjVH6gV7DdndDESvk/glHsA6dmFVI1Nk0yWiofI4pCrAr3z8iloSLc0KUemJbv43I5Z97HdzoUdE4eH5Ly3rg==", + "dev": true, "license": "MIT", "dependencies": { - "@sentry/types": "8.39.0" + "@sentry/core": "8.42.0" }, "engines": { "node": ">=14.18" @@ -6572,18 +6641,6 @@ "rxjs": "7.x" } }, - "node_modules/@swimlane/ngx-charts/node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "license": "ISC", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/@swimlane/ngx-graph": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/@swimlane/ngx-graph/-/ngx-graph-8.4.0.tgz", @@ -6669,6 +6726,15 @@ "d3-array": "2" } }, + "node_modules/@swimlane/ngx-graph/node_modules/d3-time-format": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz", + "integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-time": "1 - 2" + } + }, "node_modules/@swimlane/ngx-graph/node_modules/internmap": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", @@ -6911,9 +6977,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.1.tgz", - "integrity": "sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz", + "integrity": "sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg==", "dev": true, "license": "MIT", "dependencies": { @@ -7090,13 +7156,13 @@ } }, "node_modules/@types/node": { - "version": "22.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.1.tgz", - "integrity": "sha512-p8Yy/8sw1caA8CdRIQBG5tiLHmxtQKObCijiAa9Ez+d4+PRffM4054xbju0msf+cvhJpnFEeNjxmVT/0ipktrg==", + "version": "22.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", + "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.8" + "undici-types": "~6.20.0" } }, "node_modules/@types/node-forge": { @@ -7296,17 +7362,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.15.0.tgz", - "integrity": "sha512-+zkm9AR1Ds9uLWN3fkoeXgFppaQ+uEVtfOV62dDmsy9QCNqlRHWNEck4yarvRNrvRcHQLGfqBNui3cimoz8XAg==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.17.0.tgz", + "integrity": "sha512-HU1KAdW3Tt8zQkdvNoIijfWDMvdSweFYm4hWh+KwhPstv+sCmWb89hCIP8msFm9N1R/ooh9honpSuvqKWlYy3w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.15.0", - "@typescript-eslint/type-utils": "8.15.0", - "@typescript-eslint/utils": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0", + "@typescript-eslint/scope-manager": "8.17.0", + "@typescript-eslint/type-utils": "8.17.0", + "@typescript-eslint/utils": "8.17.0", + "@typescript-eslint/visitor-keys": "8.17.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -7329,17 +7395,27 @@ } } }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/@typescript-eslint/parser": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.15.0.tgz", - "integrity": "sha512-7n59qFpghG4uazrF9qtGKBZXn7Oz4sOMm8dwNWDQY96Xlm2oX67eipqcblDj+oY1lLCbf1oltMZFpUso66Kl1A==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.17.0.tgz", + "integrity": "sha512-Drp39TXuUlD49F7ilHHCG7TTg8IkA+hxCuULdmzWYICxGXvDXmDmWEjJYZQYgf6l/TFfYNE167m7isnc3xlIEg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.15.0", - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/typescript-estree": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0", + "@typescript-eslint/scope-manager": "8.17.0", + "@typescript-eslint/types": "8.17.0", + "@typescript-eslint/typescript-estree": "8.17.0", + "@typescript-eslint/visitor-keys": "8.17.0", "debug": "^4.3.4" }, "engines": { @@ -7359,14 +7435,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.15.0.tgz", - "integrity": "sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.17.0.tgz", + "integrity": "sha512-/ewp4XjvnxaREtqsZjF4Mfn078RD/9GmiEAtTeLQ7yFdKnqwTOgRMSvFz4et9U5RiJQ15WTGXPLj89zGusvxBg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0" + "@typescript-eslint/types": "8.17.0", + "@typescript-eslint/visitor-keys": "8.17.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7377,14 +7453,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.15.0.tgz", - "integrity": "sha512-UU6uwXDoI3JGSXmcdnP5d8Fffa2KayOhUUqr/AiBnG1Gl7+7ut/oyagVeSkh7bxQ0zSXV9ptRh/4N15nkCqnpw==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.17.0.tgz", + "integrity": "sha512-q38llWJYPd63rRnJ6wY/ZQqIzPrBCkPdpIsaCfkR3Q4t3p6sb422zougfad4TFW9+ElIFLVDzWGiGAfbb/v2qw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.15.0", - "@typescript-eslint/utils": "8.15.0", + "@typescript-eslint/typescript-estree": "8.17.0", + "@typescript-eslint/utils": "8.17.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -7405,9 +7481,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.15.0.tgz", - "integrity": "sha512-n3Gt8Y/KyJNe0S3yDCD2RVKrHBC4gTUcLTebVBXacPy091E6tNspFLKRXlk3hwT4G55nfr1n2AdFqi/XMxzmPQ==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.17.0.tgz", + "integrity": "sha512-gY2TVzeve3z6crqh2Ic7Cr+CAv6pfb0Egee7J5UAVWCpVvDI/F71wNfolIim4FE6hT15EbpZFVUj9j5i38jYXA==", "dev": true, "license": "MIT", "engines": { @@ -7419,14 +7495,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.15.0.tgz", - "integrity": "sha512-1eMp2JgNec/niZsR7ioFBlsh/Fk0oJbhaqO0jRyQBMgkz7RrFfkqF9lYYmBoGBaSiLnu8TAPQTwoTUiSTUW9dg==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.17.0.tgz", + "integrity": "sha512-JqkOopc1nRKZpX+opvKqnM3XUlM7LpFMD0lYxTqOTKQfCWAmxw45e3qlOCsEqEB2yuacujivudOFpCnqkBDNMw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0", + "@typescript-eslint/types": "8.17.0", + "@typescript-eslint/visitor-keys": "8.17.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -7448,16 +7524,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.15.0.tgz", - "integrity": "sha512-k82RI9yGhr0QM3Dnq+egEpz9qB6Un+WLYhmoNcvl8ltMEededhh7otBVVIDDsEEttauwdY/hQoSsOv13lxrFzQ==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.17.0.tgz", + "integrity": "sha512-bQC8BnEkxqG8HBGKwG9wXlZqg37RKSMY7v/X8VEWD8JG2JuTHuNK0VFvMPMUKQcbk6B+tf05k+4AShAEtCtJ/w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.15.0", - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/typescript-estree": "8.15.0" + "@typescript-eslint/scope-manager": "8.17.0", + "@typescript-eslint/types": "8.17.0", + "@typescript-eslint/typescript-estree": "8.17.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7476,13 +7552,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.15.0.tgz", - "integrity": "sha512-h8vYOulWec9LhpwfAdZf2bjr8xIp0KNKnpgqSz0qqYYKAW/QZKw3ktRndbiAtUz4acH4QLQavwZBYCc0wulA/Q==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.17.0.tgz", + "integrity": "sha512-1Hm7THLpO6ww5QU6H/Qp+AusUUl+z/CAm3cNZZ0jQvon9yicgO7Rwd+/WWRpMKLYV6p2UvdbR27c86rzCPpreg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/types": "8.17.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -8071,49 +8147,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true - }, - "node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "deprecated": "This package is no longer supported.", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/are-we-there-yet/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -8207,9 +8240,9 @@ } }, "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz", + "integrity": "sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==", "dev": true, "license": "MIT", "dependencies": { @@ -8453,7 +8486,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -8504,7 +8537,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -8516,7 +8549,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -8587,9 +8620,9 @@ } }, "node_modules/bonjour-service": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", - "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", "dev": true, "license": "MIT", "dependencies": { @@ -8704,7 +8737,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -8860,9 +8893,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001680", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001680.tgz", - "integrity": "sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==", + "version": "1.0.30001685", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001685.tgz", + "integrity": "sha512-e/kJN1EMyHQzgcMEEgoo+YTCO1NGCmIYHk5Qk8jT6AazWemS5QFKJ5ShCJlH3GZrNIdZofcNCEwZqbMjjKzmnA==", "funding": [ { "type": "opencollective", @@ -8879,24 +8912,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/canvas": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", - "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.0", - "nan": "^2.17.0", - "simple-get": "^3.0.3" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -9229,18 +9244,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "color-support": "bin.js" - } - }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -9353,15 +9356,6 @@ "node": ">=0.8" } }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true - }, "node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -9575,9 +9569,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", - "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -9938,38 +9932,17 @@ } }, "node_modules/d3-time-format": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz", - "integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-time": "1 - 2" - } - }, - "node_modules/d3-time-format/node_modules/d3-array": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", - "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", - "license": "BSD-3-Clause", - "dependencies": { - "internmap": "^1.0.0" - } - }, - "node_modules/d3-time-format/node_modules/d3-time": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz", - "integrity": "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==", - "license": "BSD-3-Clause", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", "dependencies": { - "d3-array": "2" + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" } }, - "node_modules/d3-time-format/node_modules/internmap": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", - "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", - "license": "ISC" - }, "node_modules/d3-timer": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", @@ -10049,19 +10022,6 @@ "dev": true, "license": "MIT" }, - "node_modules/decompress-response": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", - "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", - "license": "MIT", - "optional": true, - "dependencies": { - "mimic-response": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/dedent": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", @@ -10077,16 +10037,6 @@ } } }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -10188,15 +10138,6 @@ "node": ">=0.4.0" } }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -10222,7 +10163,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -10352,9 +10293,9 @@ } }, "node_modules/dompurify": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.1.tgz", - "integrity": "sha512-NBHEsc0/kzRYQd+AY6HR6B/IgsqzBABrqJbpCDQII/OK6h7B7LXzweZTDsqSW2LkTRpoxf18YUP+YjGySk6B3w==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.2.tgz", + "integrity": "sha512-YMM+erhdZ2nkZ4fTNRTSI94mb7VG7uVF5vj5Zde7tImgnhZE3R6YW/IACGIHb2ux+QkEXMhe591N+5jWOmL4Zw==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -10435,9 +10376,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.60", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.60.tgz", - "integrity": "sha512-HcraRUkTKJ+8yA3b10i9qvhUlPBRDlKjn1XGek1zDGVfAKcvi8TsUnImGqLiEm9j6ZulxXIWWIo9BmbkbCTGgA==", + "version": "1.5.68", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.68.tgz", + "integrity": "sha512-FgMdJlma0OzUYlbrtZ4AeXjKxKPk6KT8WOP8BjcqxWtlg8qyJQjRzPJzUtUn5GBg1oQ26hFs7HOOHJMYiJRnvQ==", "license": "ISC" }, "node_modules/emittery": { @@ -10524,7 +10465,7 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -10736,9 +10677,9 @@ } }, "node_modules/eslint": { - "version": "9.15.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.15.0.tgz", - "integrity": "sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==", + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.16.0.tgz", + "integrity": "sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==", "dev": true, "license": "MIT", "dependencies": { @@ -10747,7 +10688,7 @@ "@eslint/config-array": "^0.19.0", "@eslint/core": "^0.9.0", "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.15.0", + "@eslint/js": "9.16.0", "@eslint/plugin-kit": "^0.2.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -10947,6 +10888,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint-plugin-deprecation/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/eslint-plugin-deprecation/node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -11149,6 +11100,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint-plugin-jest-extended/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/eslint-plugin-jest-extended/node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -11261,6 +11222,16 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/eslint/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -11448,16 +11419,6 @@ "node": ">= 0.8.0" } }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "optional": true, - "engines": { - "node": ">=6" - } - }, "node_modules/expect": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", @@ -11856,9 +11817,9 @@ } }, "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", "dev": true, "license": "ISC" }, @@ -12037,7 +11998,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/fs-minipass": { @@ -12085,77 +12046,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "deprecated": "This package is no longer supported.", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/gauge/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/gauge/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/gauge/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true - }, - "node_modules/gauge/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -12247,13 +12137,6 @@ "get-symbol-from-current-process-h": "^1.0.1" } }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT", - "optional": true - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -12350,14 +12233,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globby/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.1.0.tgz", + "integrity": "sha512-FQoVQnqcdk4hVM4JN1eromaun4iuS34oStkdlLENLdpULsuQcTyXj8w7ayhuUfPwEYZ1ZOooOTT6fdA9Vmx/RA==", "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.3" + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -12417,11 +12313,14 @@ } }, "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.1.0.tgz", + "integrity": "sha512-QLdzI9IIO1Jg7f9GT1gXpPpXArAn6cS31R1eEZqz08Gc+uQ8/XiqHWt17Fiw+2p6oTTIq5GXEpQkAlA88YRl/Q==", "dev": true, "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7" + }, "engines": { "node": ">= 0.4" }, @@ -12430,9 +12329,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", "engines": { @@ -12442,15 +12341,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -12748,7 +12638,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -12766,9 +12656,9 @@ "license": "BSD-3-Clause" }, "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz", + "integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==", "dev": true, "license": "MIT", "engines": { @@ -12809,9 +12699,9 @@ "license": "MIT" }, "node_modules/immutable": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.2.tgz", - "integrity": "sha512-1NU7hWZDkV7hJ4PJ9dur9gTNQ4ePNPN4k9/0YhwjzykTi/+3Q5pF93YU5QoVj8BuOnhLgaY8gs0U2pj4kSYVcw==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", + "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==", "dev": true, "license": "MIT" }, @@ -13896,9 +13786,9 @@ } }, "node_modules/jest-preset-angular": { - "version": "14.3.2", - "resolved": "https://registry.npmjs.org/jest-preset-angular/-/jest-preset-angular-14.3.2.tgz", - "integrity": "sha512-Aoei1O/o7x1I6bSCpU08jGqtQ2RBq7HvNbMIo/vHHbM50v4HX1gF3sWZTkM0U0KorNkdwZeONjMsPNwHyUAKqA==", + "version": "14.4.2", + "resolved": "https://registry.npmjs.org/jest-preset-angular/-/jest-preset-angular-14.4.2.tgz", + "integrity": "sha512-BYYv0FaTDfBNh8WyA9mpOV3krfw20kurBGK8INZUnv7KZDAWZuQtCET4TwTWxSNQ9jS1OX1+a5weCm/bTDDM1A==", "dev": true, "license": "MIT", "dependencies": { @@ -13916,10 +13806,9 @@ "esbuild": ">=0.15.13" }, "peerDependencies": { - "@angular-devkit/build-angular": ">=15.0.0 <19.0.0", - "@angular/compiler-cli": ">=15.0.0 <19.0.0", - "@angular/core": ">=15.0.0 <19.0.0", - "@angular/platform-browser-dynamic": ">=15.0.0 <19.0.0", + "@angular/compiler-cli": ">=15.0.0 <20.0.0", + "@angular/core": ">=15.0.0 <20.0.0", + "@angular/platform-browser-dynamic": ">=15.0.0 <20.0.0", "jest": "^29.0.0", "typescript": ">=4.8" } @@ -15271,9 +15160,9 @@ } }, "node_modules/memfs": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.14.0.tgz", - "integrity": "sha512-JUeY0F/fQZgIod31Ja1eJgiSxLn7BfQlCnqhwXFBzFHEw63OdLK7VJUJ7bnzNsWgCyoUP5tEp1VRY8rDaYzqOA==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.14.1.tgz", + "integrity": "sha512-Fq5CMEth+2iprLJ5mNizRcWuiwRZYjNkUD0zKk224jZunE9CRacTRDK8QLALbMBlNX2y3nY6lKZbesCwDwacig==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -15414,19 +15303,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mimic-response": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/mini-css-extract-plugin": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.0.tgz", @@ -15475,7 +15351,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "devOptional": true, + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -15668,13 +15544,6 @@ "node": ">=10" } }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT", - "optional": true - }, "node_modules/mobile-drag-drop": { "version": "3.0.0-rc.0", "resolved": "https://registry.npmjs.org/mobile-drag-drop/-/mobile-drag-drop-3.0.0-rc.0.tgz", @@ -15780,19 +15649,10 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/nan": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", - "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true, "funding": [ { @@ -15808,13 +15668,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/napi-build-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", - "license": "MIT", - "optional": true - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -15941,103 +15794,36 @@ } } }, - "node_modules/nice-napi": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", - "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "!win32" - ], - "dependencies": { - "node-addon-api": "^3.0.0", - "node-gyp-build": "^4.2.2" - } - }, - "node_modules/nice-napi/node_modules/node-addon-api": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/node-abi": { - "version": "3.71.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.71.0.tgz", - "integrity": "sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==", - "license": "MIT", - "optional": true, - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-addon-api": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", - "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "node_modules/nice-napi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", + "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", "dev": true, + "hasInstallScript": true, "license": "MIT", "optional": true, - "peer": true + "os": [ + "!win32" + ], + "dependencies": { + "node-addon-api": "^3.0.0", + "node-gyp-build": "^4.2.2" + } }, - "node_modules/node-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "node_modules/nice-napi/node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true + "license": "MIT", + "optional": true }, - "node_modules/node-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } + "license": "MIT" }, "node_modules/node-forge": { "version": "1.3.1", @@ -16050,9 +15836,9 @@ } }, "node_modules/node-gyp": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.2.0.tgz", - "integrity": "sha512-sp3FonBAaFe4aYTcFdZUn2NYkbP7xroPGYvQmP4Nl5PxamznItBnNCgjrVTKrEfQynInMsJvZrdmqUnysCJ8rw==", + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.3.1.tgz", + "integrity": "sha512-Pp3nFHBThHzVtNY7U6JfPjvT/DTE8+o/4xKsLQtBoU+j2HLsGlhcfzflAoUreaJbNmYnX+LlLi0qjV8kpyO6xQ==", "dev": true, "license": "MIT", "dependencies": { @@ -16075,9 +15861,9 @@ } }, "node_modules/node-gyp-build": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.3.tgz", - "integrity": "sha512-EMS95CMJzdoSKoIiXo8pxKoL8DYxwIZXYlLmgPb8KUv794abpnLK6ynsCAWNliOjREKruYKdzbh76HHYUHX7nw==", + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", "dev": true, "license": "MIT", "bin": { @@ -16333,22 +16119,6 @@ "node": ">=8" } }, - "node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "deprecated": "This package is no longer supported.", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -16363,16 +16133,16 @@ } }, "node_modules/nwsapi": { - "version": "2.2.13", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.13.tgz", - "integrity": "sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==", + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.16.tgz", + "integrity": "sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==", "dev": true, "license": "MIT" }, "node_modules/nx": { - "version": "20.1.1", - "resolved": "https://registry.npmjs.org/nx/-/nx-20.1.1.tgz", - "integrity": "sha512-bLDEDBUuAvFC5b74QUnmJxUHTRa0mkc2wRPmb2rN3d1VlTFjzKTT9ClJTR1emp/DDO620zyAmVCDVKmnSZNFoQ==", + "version": "20.1.4", + "resolved": "https://registry.npmjs.org/nx/-/nx-20.1.4.tgz", + "integrity": "sha512-hyvGYxTzBkPxSXAB2tuqdv9TpVde5xOdGalsIdhF7j7PI3nwPpqtc3y28YTgRgpxtOE1Y6BfDNkXMO1SW0xu2w==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -16415,16 +16185,16 @@ "nx-cloud": "bin/nx-cloud.js" }, "optionalDependencies": { - "@nx/nx-darwin-arm64": "20.1.1", - "@nx/nx-darwin-x64": "20.1.1", - "@nx/nx-freebsd-x64": "20.1.1", - "@nx/nx-linux-arm-gnueabihf": "20.1.1", - "@nx/nx-linux-arm64-gnu": "20.1.1", - "@nx/nx-linux-arm64-musl": "20.1.1", - "@nx/nx-linux-x64-gnu": "20.1.1", - "@nx/nx-linux-x64-musl": "20.1.1", - "@nx/nx-win32-arm64-msvc": "20.1.1", - "@nx/nx-win32-x64-msvc": "20.1.1" + "@nx/nx-darwin-arm64": "20.1.4", + "@nx/nx-darwin-x64": "20.1.4", + "@nx/nx-freebsd-x64": "20.1.4", + "@nx/nx-linux-arm-gnueabihf": "20.1.4", + "@nx/nx-linux-arm64-gnu": "20.1.4", + "@nx/nx-linux-arm64-musl": "20.1.4", + "@nx/nx-linux-x64-gnu": "20.1.4", + "@nx/nx-linux-x64-musl": "20.1.4", + "@nx/nx-win32-arm64-msvc": "20.1.4", + "@nx/nx-win32-x64-msvc": "20.1.4" }, "peerDependencies": { "@swc-node/register": "^1.8.0", @@ -16469,6 +16239,16 @@ "dev": true, "license": "MIT" }, + "node_modules/nx/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/nx/node_modules/is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", @@ -16674,7 +16454,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -17143,16 +16923,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/path2d": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/path2d/-/path2d-0.2.2.tgz", - "integrity": "sha512-+vnG6S4dYcYxZd+CZxzXCNKdELYZSKfohrk98yajCo1PtRoDgCTrrwOvK1GT0UoAdVszagDVllQc0U1vaX4NUQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=6" - } - }, "node_modules/pdf-lib": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", @@ -17172,41 +16942,17 @@ "license": "0BSD" }, "node_modules/pdfjs-dist": { - "version": "4.8.69", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.8.69.tgz", - "integrity": "sha512-IHZsA4T7YElCKNNXtiLgqScw4zPd3pG9do8UrznC757gMd7UPeHSL2qwNNMJo4r79fl8oj1Xx+1nh2YkzdMpLQ==", + "version": "4.9.155", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.9.155.tgz", + "integrity": "sha512-epRZn6DQQKCOEqbmFsxkiMBm1MHaNrnr6T4VBNP0bsDvdJdmrWcZbS5cgJXW68P0d3uJTlFhF6Wms2tlSgPYig==", "license": "Apache-2.0", "engines": { - "node": ">=18" + "node": ">=20" }, "optionalDependencies": { - "canvas": "^3.0.0-rc2", - "path2d": "^0.2.1" - } - }, - "node_modules/pdfjs-dist/node_modules/canvas": { - "version": "3.0.0-rc2", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.0.0-rc2.tgz", - "integrity": "sha512-esx4bYDznnqgRX4G8kaEaf0W3q8xIc51WpmrIitDzmcoEgwnv9wSKdzT6UxWZ4wkVu5+ileofppX0TpyviJRdQ==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-addon-api": "^7.0.0", - "prebuild-install": "^7.1.1", - "simple-get": "^3.0.3" - }, - "engines": { - "node": "^18.12.0 || >= 20.9.0" + "@napi-rs/canvas": "^0.1.64" } }, - "node_modules/pdfjs-dist/node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT", - "optional": true - }, "node_modules/pepjs": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/pepjs/-/pepjs-0.5.3.tgz", @@ -17531,88 +17277,6 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, - "node_modules/prebuild-install": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", - "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^1.0.1", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/prebuild-install/node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/prebuild-install/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/prebuild-install/node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -17624,9 +17288,9 @@ } }, "node_modules/prettier": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", - "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "dev": true, "license": "MIT", "bin": { @@ -17787,17 +17451,6 @@ "license": "MIT", "optional": true }, - "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "license": "MIT", - "optional": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -17925,39 +17578,6 @@ "node": ">=0.10.0" } }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "optional": true, - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC", - "optional": true - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -18199,16 +17819,16 @@ "license": "MIT" }, "node_modules/regexpu-core": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.1.1.tgz", - "integrity": "sha512-k67Nb9jvwJcJmVpw0jPttR1/zVfnKf8Km0IPatrU/zJ5XeG3+Slx0xLXs9HByJSzXzrlz5EDvN6yLNMDc2qdnw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", + "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", "dev": true, "license": "MIT", "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.0", "regjsgen": "^0.8.0", - "regjsparser": "^0.11.0", + "regjsparser": "^0.12.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.1.0" }, @@ -18224,9 +17844,9 @@ "license": "MIT" }, "node_modules/regjsparser": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.11.2.tgz", - "integrity": "sha512-3OGZZ4HoLJkkAZx/48mTXJNlmqTGOzc0o9OWQPuWpkOlXXPbyN6OafCcoXUnBqE2D3f/T5L+pWc1kdEmnfnRsA==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -18368,9 +17988,9 @@ } }, "node_modules/resolve.exports": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", - "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", "dev": true, "license": "MIT", "engines": { @@ -18668,9 +18288,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.81.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.81.0.tgz", - "integrity": "sha512-Q4fOxRfhmv3sqCLoGfvrC9pRV8btc0UtqL9mN6Yrv6Qi9ScL55CVH1vlPP863ISLEEMNLLuu9P+enCeGHlnzhA==", + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.82.0.tgz", + "integrity": "sha512-j4GMCTa8elGyN9A7x7bEglx0VgSpNUG4W4wNedQ33wSMdnkqQCT8HTwOaVSV4e6yQovcu/3Oc4coJP/l0xhL2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -19013,15 +18633,6 @@ "node": ">= 18" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -19107,11 +18718,14 @@ } }, "node_modules/shell-quote": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", - "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", + "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", "dev": true, "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -19166,39 +18780,6 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true - }, - "node_modules/simple-get": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", - "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", - "license": "MIT", - "optional": true, - "dependencies": { - "decompress-response": "^4.2.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, "node_modules/simple-statistics": { "version": "7.8.7", "resolved": "https://registry.npmjs.org/simple-statistics/-/simple-statistics-7.8.7.tgz", @@ -19874,31 +19455,11 @@ "node": ">=10" } }, - "node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "license": "MIT", - "optional": true, - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-fs/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC", - "optional": true - }, "node_modules/tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "bl": "^4.0.3", @@ -19915,7 +19476,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -20180,22 +19741,22 @@ "license": "MIT" }, "node_modules/tldts": { - "version": "6.1.61", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.61.tgz", - "integrity": "sha512-rv8LUyez4Ygkopqn+M6OLItAOT9FF3REpPQDkdMx5ix8w4qkuE7Vo2o/vw1nxKQYmJDV8JpAMJQr1b+lTKf0FA==", + "version": "6.1.65", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.65.tgz", + "integrity": "sha512-xU9gLTfAGsADQ2PcWee6Hg8RFAv0DnjMGVJmDnUmI8a9+nYmapMQix4afwrdaCtT+AqP4MaxEzu7cCrYmBPbzQ==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^6.1.61" + "tldts-core": "^6.1.65" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.61", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.61.tgz", - "integrity": "sha512-In7VffkDWUPgwa+c9picLUxvb0RltVwTkSgMNFgvlGSWveCzGBemBqTsgJCL4EDFWZ6WH0fKTsot6yNhzy3ZzQ==", + "version": "6.1.65", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.65.tgz", + "integrity": "sha512-Uq5t0N0Oj4nQSbU8wFN1YYENvMthvwU13MQrMJRspYCGLSAZjAfoBOJki5IQpnBM/WFskxxC/gIOTwaedmHaSg==", "dev": true, "license": "MIT" }, @@ -20315,9 +19876,9 @@ } }, "node_modules/ts-api-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.0.tgz", - "integrity": "sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", "dev": true, "license": "MIT", "engines": { @@ -20509,19 +20070,6 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, "node_modules/turndown": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.0.tgz", @@ -20656,9 +20204,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, "license": "MIT" }, @@ -21910,56 +21458,6 @@ "node": ">= 8" } }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "node_modules/wide-align/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/wide-align/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/wide-align/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", @@ -22079,7 +21577,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { diff --git a/package.json b/package.json index e96395d0ca15..8c2501ed817e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "artemis", - "version": "7.7.4", + "version": "7.7.5", "description": "Interactive Learning with Individual Feedback", "private": true, "license": "MIT", @@ -13,18 +13,18 @@ "node_modules" ], "dependencies": { - "@angular/animations": "18.2.12", + "@angular/animations": "18.2.13", "@angular/cdk": "18.2.13", - "@angular/common": "18.2.12", - "@angular/compiler": "18.2.12", - "@angular/core": "18.2.12", - "@angular/forms": "18.2.12", - "@angular/localize": "18.2.12", + "@angular/common": "18.2.13", + "@angular/compiler": "18.2.13", + "@angular/core": "18.2.13", + "@angular/forms": "18.2.13", + "@angular/localize": "18.2.13", "@angular/material": "18.2.13", - "@angular/platform-browser": "18.2.12", - "@angular/platform-browser-dynamic": "18.2.12", - "@angular/router": "18.2.12", - "@angular/service-worker": "18.2.12", + "@angular/platform-browser": "18.2.13", + "@angular/platform-browser-dynamic": "18.2.13", + "@angular/router": "18.2.13", + "@angular/service-worker": "18.2.13", "@ctrl/ngx-emoji-mart": "9.2.0", "@danielmoncada/angular-datetime-picker": "18.1.0", "@fingerprintjs/fingerprintjs": "4.5.1", @@ -36,7 +36,7 @@ "@ng-bootstrap/ng-bootstrap": "17.0.1", "@ngx-translate/core": "16.0.3", "@ngx-translate/http-loader": "16.0.0", - "@sentry/angular": "8.39.0", + "@sentry/angular": "8.42.0", "@siemens/ngx-datatable": "22.4.1", "@swimlane/ngx-charts": "21.0.0", "@swimlane/ngx-graph": "8.4.0", @@ -48,7 +48,7 @@ "crypto-js": "4.2.0", "dayjs": "1.11.13", "diff-match-patch-typescript": "1.1.0", - "dompurify": "3.2.1", + "dompurify": "3.2.2", "emoji-js": "3.8.0", "export-to-csv": "1.4.0", "fast-json-patch": "3.1.1", @@ -68,7 +68,7 @@ "ngx-webstorage": "18.0.0", "papaparse": "5.4.1", "pdf-lib": "1.17.1", - "pdfjs-dist": "4.8.69", + "pdfjs-dist": "4.9.155", "rxjs": "7.8.1", "simple-statistics": "7.8.7", "smoothscroll-polyfill": "0.4.4", @@ -91,14 +91,14 @@ "d3-transition": "^3.0.1" }, "@typescript-eslint/utils": { - "eslint": "^9.15.0" + "eslint": "^9.16.0" }, "braces": "3.0.3", "cookie": "1.0.1", "critters": "0.0.25", "debug": "4.3.7", "eslint-plugin-deprecation": { - "eslint": "^9.15.0" + "eslint": "^9.16.0" }, "express": "5.0.1", "jsdom": "25.0.1", @@ -122,9 +122,9 @@ "@angular-eslint/schematics": "18.4.1", "@angular-eslint/template-parser": "18.4.1", "@angular/cli": "18.2.12", - "@angular/compiler-cli": "18.2.12", - "@angular/language-service": "18.2.12", - "@sentry/types": "8.39.0", + "@angular/compiler-cli": "18.2.13", + "@angular/language-service": "18.2.13", + "@sentry/types": "8.42.0", "@types/crypto-js": "4.2.2", "@types/d3-shape": "3.1.6", "@types/dompurify": "3.0.5", @@ -132,15 +132,15 @@ "@types/jest": "29.5.14", "@types/lodash-es": "4.17.12", "@types/markdown-it": "14.1.2", - "@types/node": "22.9.1", + "@types/node": "22.10.1", "@types/papaparse": "5.3.15", "@types/smoothscroll-polyfill": "0.3.4", "@types/sockjs-client": "1.5.4", "@types/turndown": "5.0.5", "@types/uuid": "10.0.0", - "@typescript-eslint/eslint-plugin": "8.15.0", - "@typescript-eslint/parser": "8.15.0", - "eslint": "9.15.0", + "@typescript-eslint/eslint-plugin": "8.17.0", + "@typescript-eslint/parser": "8.17.0", + "eslint": "9.16.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-deprecation": "3.0.0", "eslint-plugin-jest": "28.9.0", @@ -154,13 +154,13 @@ "jest-extended": "4.0.2", "jest-fail-on-console": "3.3.1", "jest-junit": "16.0.0", - "jest-preset-angular": "14.3.2", + "jest-preset-angular": "14.4.2", "lint-staged": "15.2.10", "ngxtension": "4.1.0", "ng-mocks": "14.13.1", - "prettier": "3.3.3", + "prettier": "3.4.2", "rimraf": "6.0.1", - "sass": "1.81.0", + "sass": "1.82.0", "ts-jest": "29.2.5", "typescript": "5.5.4", "weak-napi": "2.0.2" diff --git a/ruleset.xml b/ruleset.xml index caa96b49676c..07ea82074ae5 100644 --- a/ruleset.xml +++ b/ruleset.xml @@ -8,15 +8,13 @@ http://pmd.sourceforge.net/ruleset/2.0.0 "> - Allows underscores in JUnit 4 and 5 test methods + Allows underscores in JUnit 5 test methods Template from https://pmd.github.io/pmd-6.40.0/pmd_userdocs_making_rulesets.html#creating-a-ruleset - - diff --git a/settings.gradle b/settings.gradle index c99752089fcd..4079e5a4cc6e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,9 +14,6 @@ pluginManagement { rootProject.name = 'Artemis' -// needed for rest call and endpoint analysis -include 'supporting_scripts:analysis-of-endpoint-connections' - // needed for programming exercise templates DirectoryScanner.removeDefaultExclude "**/.gitattributes" -DirectoryScanner.removeDefaultExclude "**/.gitignore" \ No newline at end of file +DirectoryScanner.removeDefaultExclude "**/.gitignore" diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/repository/ResultRepository.java b/src/main/java/de/tum/cit/aet/artemis/assessment/repository/ResultRepository.java index 5e70f4c83668..e0af5465f3db 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/repository/ResultRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/repository/ResultRepository.java @@ -104,6 +104,8 @@ default List findLatestAutomaticResultsWithEagerFeedbacksTestCasesForExe Optional findFirstByParticipationIdOrderByCompletionDateDesc(long participationId); + Optional findFirstByParticipationIdAndAssessmentTypeOrderByCompletionDateDesc(long participationId, AssessmentType assessmentType); + @EntityGraph(type = LOAD, attributePaths = { "feedbacks", "feedbacks.testCase" }) Optional findResultWithFeedbacksAndTestCasesById(long resultId); diff --git a/src/main/java/de/tum/cit/aet/artemis/athena/dto/ModelingFeedbackDTO.java b/src/main/java/de/tum/cit/aet/artemis/athena/dto/ModelingFeedbackDTO.java index e4dd2926a0eb..ae564044bcdf 100644 --- a/src/main/java/de/tum/cit/aet/artemis/athena/dto/ModelingFeedbackDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/athena/dto/ModelingFeedbackDTO.java @@ -1,7 +1,5 @@ package de.tum.cit.aet.artemis.athena.dto; -import java.util.List; - import jakarta.validation.constraints.NotNull; import com.fasterxml.jackson.annotation.JsonInclude; @@ -13,7 +11,7 @@ */ @JsonInclude(JsonInclude.Include.NON_EMPTY) public record ModelingFeedbackDTO(long id, long exerciseId, long submissionId, String title, String description, double credits, Long structuredGradingInstructionId, - List elementIds) implements FeedbackBaseDTO { + String reference) implements FeedbackBaseDTO { /** * Creates a ModelingFeedbackDTO from a Feedback object @@ -30,6 +28,6 @@ public static ModelingFeedbackDTO of(long exerciseId, long submissionId, @NotNul } return new ModelingFeedbackDTO(feedback.getId(), exerciseId, submissionId, feedback.getText(), feedback.getDetailText(), feedback.getCredits(), gradingInstructionId, - List.of(feedback.getReference())); + feedback.getReference()); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/repository/CourseCompetencyRepository.java b/src/main/java/de/tum/cit/aet/artemis/atlas/repository/CourseCompetencyRepository.java index bba103f436d9..038d6bd63346 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/repository/CourseCompetencyRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/repository/CourseCompetencyRepository.java @@ -295,5 +295,14 @@ default CourseCompetency findByIdWithLectureUnitsAndExercisesElseThrow(long comp List findByCourseIdOrderById(long courseId); + @Query(""" + SELECT c + FROM CourseCompetency c + WHERE c.course.id = :courseId + AND (SIZE(c.lectureUnitLinks) > 0 OR SIZE(c.exerciseLinks) > 0) + ORDER BY c.id + """) + List findByCourseIdAndLinkedToLearningObjectOrderById(@Param("courseId") long courseId); + boolean existsByIdAndCourseId(long competencyId, long courseId); } diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CourseCompetencyService.java b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CourseCompetencyService.java index 88cad15f1000..77c1eda8385f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CourseCompetencyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CourseCompetencyService.java @@ -124,10 +124,17 @@ public CourseCompetency findCompetencyWithExercisesAndLectureUnitsAndProgressFor * * @param courseId The id of the course for which to fetch the competencies * @param userId The id of the user for which to fetch the progress + * @param filter Whether to filter out competencies that are not linked to any learning objects * @return The found competency */ - public List findCourseCompetenciesWithProgressForUserByCourseId(Long courseId, Long userId) { - List competencies = courseCompetencyRepository.findByCourseIdOrderById(courseId); + public List findCourseCompetenciesWithProgressForUserByCourseId(long courseId, long userId, boolean filter) { + List competencies; + if (filter) { + competencies = courseCompetencyRepository.findByCourseIdAndLinkedToLearningObjectOrderById(courseId); + } + else { + competencies = courseCompetencyRepository.findByCourseIdOrderById(courseId); + } return findProgressForCompetenciesAndUser(competencies, userId); } diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/web/CourseCompetencyResource.java b/src/main/java/de/tum/cit/aet/artemis/atlas/web/CourseCompetencyResource.java index 8e93c6c73090..469ba555c9b3 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/web/CourseCompetencyResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/web/CourseCompetencyResource.java @@ -162,15 +162,16 @@ public ResponseEntity getCourseCompetency(@PathVariable long c /** * GET courses/:courseId/course-competencies : gets all the course competencies of a course * - * @param courseId the id of the course for which the competencies should be fetched + * @param courseId The id of the course for which the competencies should be fetched + * @param filter Whether to filter out competencies that are not linked to any learning objects * @return the ResponseEntity with status 200 (OK) and with body the found competencies */ @GetMapping("courses/{courseId}/course-competencies") @EnforceAtLeastStudentInCourse - public ResponseEntity> getCourseCompetenciesWithProgress(@PathVariable long courseId) { + public ResponseEntity> getCourseCompetenciesWithProgress(@PathVariable long courseId, @RequestParam(defaultValue = "false") boolean filter) { log.debug("REST request to get competencies for course with id: {}", courseId); User user = userRepository.getUserWithGroupsAndAuthorities(); - final var competencies = courseCompetencyService.findCourseCompetenciesWithProgressForUserByCourseId(courseId, user.getId()); + final var competencies = courseCompetencyService.findCourseCompetenciesWithProgressForUserByCourseId(courseId, user.getId(), filter); return ResponseEntity.ok(competencies); } diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/BuildAgentConfiguration.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/BuildAgentConfiguration.java index b58d6dbd2fa8..186ca2433639 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/BuildAgentConfiguration.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/BuildAgentConfiguration.java @@ -23,8 +23,8 @@ import com.github.dockerjava.core.DefaultDockerClientConfig; import com.github.dockerjava.core.DockerClientConfig; import com.github.dockerjava.core.DockerClientImpl; -import com.github.dockerjava.httpclient5.ApacheDockerHttpClient; import com.github.dockerjava.transport.DockerHttpClient; +import com.github.dockerjava.zerodep.ZerodepDockerHttpClient; import com.google.common.util.concurrent.ThreadFactoryBuilder; import de.tum.cit.aet.artemis.core.config.ProgrammingLanguageConfiguration; @@ -151,7 +151,7 @@ public ExecutorService localCIBuildExecutorService() { public DockerClient dockerClient() { log.debug("Create bean dockerClient"); DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().withDockerHost(dockerConnectionUri).build(); - DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder().dockerHost(config.getDockerHost()).sslConfig(config.getSSLConfig()).build(); + DockerHttpClient httpClient = new ZerodepDockerHttpClient.Builder().dockerHost(config.getDockerHost()).sslConfig(config.getSSLConfig()).build(); DockerClient dockerClient = DockerClientImpl.getInstance(config, httpClient); log.debug("Docker client created with connection URI: {}", dockerConnectionUri); diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildConfig.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildConfig.java index 9a41fc6fdc20..34d139aa6f19 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildConfig.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildConfig.java @@ -15,7 +15,8 @@ @JsonInclude(JsonInclude.Include.NON_EMPTY) public record BuildConfig(String buildScript, String dockerImage, String commitHashToBuild, String assignmentCommitHash, String testCommitHash, String branch, ProgrammingLanguage programmingLanguage, ProjectType projectType, boolean scaEnabled, boolean sequentialTestRunsEnabled, boolean testwiseCoverageEnabled, - List resultPaths, int timeoutSeconds, String assignmentCheckoutPath, String testCheckoutPath, String solutionCheckoutPath) implements Serializable { + List resultPaths, int timeoutSeconds, String assignmentCheckoutPath, String testCheckoutPath, String solutionCheckoutPath, DockerRunConfig dockerRunConfig) + implements Serializable { @Override public String dockerImage() { diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/DockerFlagsDTO.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/DockerFlagsDTO.java new file mode 100644 index 000000000000..bb10c5ddf313 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/DockerFlagsDTO.java @@ -0,0 +1,6 @@ +package de.tum.cit.aet.artemis.buildagent.dto; + +import java.util.Map; + +public record DockerFlagsDTO(String network, Map env) { +} diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/DockerRunConfig.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/DockerRunConfig.java new file mode 100644 index 000000000000..2b45273e13fd --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/DockerRunConfig.java @@ -0,0 +1,7 @@ +package de.tum.cit.aet.artemis.buildagent.dto; + +import java.io.Serializable; +import java.util.List; + +public record DockerRunConfig(boolean isNetworkDisabled, List env) implements Serializable { +} diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobContainerService.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobContainerService.java index cfe5a1ab01e3..3c7ff12881cd 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobContainerService.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobContainerService.java @@ -85,12 +85,13 @@ public BuildJobContainerService(DockerClient dockerClient, HostConfig hostConfig /** * Configure a container with the Docker image, the container name, optional proxy config variables, and set the command that runs when the container starts. * - * @param containerName the name of the container to be created - * @param image the Docker image to use for the container - * @param buildScript the build script to be executed in the container + * @param containerName the name of the container to be created + * @param image the Docker image to use for the container + * @param buildScript the build script to be executed in the container + * @param exerciseEnvVars the environment variables provided by the instructor * @return {@link CreateContainerResponse} that can be used to start the container */ - public CreateContainerResponse configureContainer(String containerName, String image, String buildScript) { + public CreateContainerResponse configureContainer(String containerName, String image, String buildScript, List exerciseEnvVars) { List envVars = new ArrayList<>(); if (useSystemProxy) { envVars.add("HTTP_PROXY=" + httpProxy); @@ -98,6 +99,9 @@ public CreateContainerResponse configureContainer(String containerName, String i envVars.add("NO_PROXY=" + noProxy); } envVars.add("SCRIPT=" + buildScript); + if (exerciseEnvVars != null && !exerciseEnvVars.isEmpty()) { + envVars.addAll(exerciseEnvVars); + } return dockerClient.createContainerCmd(image).withName(containerName).withHostConfig(hostConfig).withEnv(envVars) // Command to run when the container starts. This is the command that will be executed in the container's main process, which runs in the foreground and blocks the // container from exiting until it finishes. @@ -121,11 +125,23 @@ public void startContainer(String containerId) { /** * Run the script in the container and wait for it to finish before returning. * - * @param containerId the id of the container in which the script should be run - * @param buildJobId the id of the build job that is currently being executed + * @param containerId the id of the container in which the script should be run + * @param buildJobId the id of the build job that is currently being executed + * @param isNetworkDisabled whether the network should be disabled for the container */ + public void runScriptInContainer(String containerId, String buildJobId, boolean isNetworkDisabled) { + if (isNetworkDisabled) { + log.info("disconnecting container with id {} from network", containerId); + try { + dockerClient.disconnectFromNetworkCmd().withContainerId(containerId).withNetworkId("bridge").exec(); + } + catch (Exception e) { + log.error("Failed to disconnect container with id {} from network: {}", containerId, e.getMessage()); + buildLogsMap.appendBuildLogEntry(buildJobId, "Failed to disconnect container from default network 'bridge': " + e.getMessage()); + throw new LocalCIException("Failed to disconnect container from default network 'bridge': " + e.getMessage()); + } + } - public void runScriptInContainer(String containerId, String buildJobId) { log.info("Started running the build script for build job in container with id {}", containerId); // The "sh script.sh" execution command specified here is run inside the container as an additional process. This command runs in the background, independent of the // container's @@ -190,6 +206,8 @@ public void stopContainer(String containerName) { // Get the container ID. String containerId = container.getId(); + log.info("Stopping container with id {}", containerId); + // Create a file "stop_container.txt" in the root directory of the container to indicate that the test results have been extracted or that the container should be stopped // for some other reason. // The container's main process is waiting for this file to appear and then stops the main process, thus stopping and removing the container. @@ -448,9 +466,4 @@ private Container getContainerForName(String containerName) { List containers = dockerClient.listContainersCmd().withShowAll(true).exec(); return containers.stream().filter(container -> container.getNames()[0].equals("/" + containerName)).findFirst().orElse(null); } - - private String getParentFolderPath(String path) { - Path parentPath = Paths.get(path).normalize().getParent(); - return parentPath != null ? parentPath.toString() : ""; - } } diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobExecutionService.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobExecutionService.java index 7c789cfafb28..2fb2c805bf13 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobExecutionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobExecutionService.java @@ -232,10 +232,18 @@ public BuildResult runBuildJob(BuildJobQueueItem buildJob, String containerName) index++; } - CreateContainerResponse container = buildJobContainerService.configureContainer(containerName, buildJob.buildConfig().dockerImage(), buildJob.buildConfig().buildScript()); + List envVars = null; + boolean isNetworkDisabled = false; + if (buildJob.buildConfig().dockerRunConfig() != null) { + envVars = buildJob.buildConfig().dockerRunConfig().env(); + isNetworkDisabled = buildJob.buildConfig().dockerRunConfig().isNetworkDisabled(); + } + + CreateContainerResponse container = buildJobContainerService.configureContainer(containerName, buildJob.buildConfig().dockerImage(), buildJob.buildConfig().buildScript(), + envVars); return runScriptAndParseResults(buildJob, containerName, container.getId(), assignmentRepoUri, testsRepoUri, solutionRepoUri, auxiliaryRepositoriesUris, - assignmentRepositoryPath, testsRepositoryPath, solutionRepositoryPath, auxiliaryRepositoriesPaths, assignmentCommitHash, testCommitHash); + assignmentRepositoryPath, testsRepositoryPath, solutionRepositoryPath, auxiliaryRepositoriesPaths, assignmentCommitHash, testCommitHash, isNetworkDisabled); } /** @@ -270,7 +278,7 @@ public BuildResult runBuildJob(BuildJobQueueItem buildJob, String containerName) private BuildResult runScriptAndParseResults(BuildJobQueueItem buildJob, String containerName, String containerId, VcsRepositoryUri assignmentRepositoryUri, VcsRepositoryUri testRepositoryUri, VcsRepositoryUri solutionRepositoryUri, VcsRepositoryUri[] auxiliaryRepositoriesUris, Path assignmentRepositoryPath, Path testsRepositoryPath, Path solutionRepositoryPath, Path[] auxiliaryRepositoriesPaths, @Nullable String assignmentRepoCommitHash, - @Nullable String testRepoCommitHash) { + @Nullable String testRepoCommitHash, boolean isNetworkDisabled) { long timeNanoStart = System.nanoTime(); @@ -292,7 +300,7 @@ private BuildResult runScriptAndParseResults(BuildJobQueueItem buildJob, String buildLogsMap.appendBuildLogEntry(buildJob.id(), msg); log.debug(msg); - buildJobContainerService.runScriptInContainer(containerId, buildJob.id()); + buildJobContainerService.runScriptInContainer(containerId, buildJob.id(), isNetworkDisabled); msg = "~~~~~~~~~~~~~~~~~~~~ Finished Executing Build Script for Build job " + buildJob.id() + " ~~~~~~~~~~~~~~~~~~~~"; buildLogsMap.appendBuildLogEntry(buildJob.id(), msg); @@ -300,10 +308,18 @@ private BuildResult runScriptAndParseResults(BuildJobQueueItem buildJob, String ZonedDateTime buildCompletedDate = ZonedDateTime.now(); + msg = "~~~~~~~~~~~~~~~~~~~~ Moving test results to specified directory for build job " + buildJob.id() + " ~~~~~~~~~~~~~~~~~~~~"; + buildLogsMap.appendBuildLogEntry(buildJob.id(), msg); + log.debug(msg); + buildJobContainerService.moveResultsToSpecifiedDirectory(containerId, buildJob.buildConfig().resultPaths(), LOCALCI_WORKING_DIRECTORY + LOCALCI_RESULTS_DIRECTORY); // Get an input stream of the test result files. + msg = "~~~~~~~~~~~~~~~~~~~~ Collecting test results from container " + containerId + " for build job " + buildJob.id() + " ~~~~~~~~~~~~~~~~~~~~"; + buildLogsMap.appendBuildLogEntry(buildJob.id(), msg); + log.info(msg); + TarArchiveInputStream testResultsTarInputStream; try { @@ -341,6 +357,10 @@ private BuildResult runScriptAndParseResults(BuildJobQueueItem buildJob, String } } + msg = "~~~~~~~~~~~~~~~~~~~~ Parsing test results for build job " + buildJob.id() + " ~~~~~~~~~~~~~~~~~~~~"; + buildLogsMap.appendBuildLogEntry(buildJob.id(), msg); + log.info(msg); + BuildResult buildResult; try { buildResult = parseTestResults(testResultsTarInputStream, buildJob.buildConfig().branch(), assignmentRepoCommitHash, testRepoCommitHash, buildCompletedDate, @@ -354,7 +374,7 @@ private BuildResult runScriptAndParseResults(BuildJobQueueItem buildJob, String } msg = "Building and testing submission for repository " + assignmentRepositoryUri.repositorySlug() + " and commit hash " + assignmentRepoCommitHash + " took " - + TimeLogUtil.formatDurationFrom(timeNanoStart); + + TimeLogUtil.formatDurationFrom(timeNanoStart) + " for build job " + buildJob.id(); buildLogsMap.appendBuildLogEntry(buildJob.id(), msg); log.info(msg); diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobManagementService.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobManagementService.java index be480892b8e3..d4142e21f24a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobManagementService.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobManagementService.java @@ -16,6 +16,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; @@ -176,6 +177,9 @@ public CompletableFuture executeBuildJob(BuildJobQueueItem buildJob } else { finishBuildJobExceptionally(buildJobItem.id(), containerName, e); + if (e instanceof TimeoutException) { + logTimedOutBuildJob(buildJobItem, buildJobTimeoutSeconds); + } throw new CompletionException(e); } } @@ -188,6 +192,18 @@ public CompletableFuture executeBuildJob(BuildJobQueueItem buildJob })); } + private void logTimedOutBuildJob(BuildJobQueueItem buildJobItem, int buildJobTimeoutSeconds) { + String msg = "Timed out after " + buildJobTimeoutSeconds + " seconds. " + + "This may be due to an infinite loop or inefficient code. Please review your code for potential issues. " + + "If the problem persists, contact your instructor for assistance. (Build job ID: " + buildJobItem.id() + ")"; + buildLogsMap.appendBuildLogEntry(buildJobItem.id(), msg); + log.warn(msg); + + msg = "Executing build job with id " + buildJobItem.id() + " timed out after " + buildJobTimeoutSeconds + " seconds." + + "This may be due to strict timeout settings. Consider increasing the exercise timeout and applying stricter timeout constraints within the test cases using @StrictTimeout."; + buildLogsMap.appendBuildLogEntry(buildJobItem.id(), msg); + } + Set getRunningBuildJobIds() { return Set.copyOf(runningFutures.keySet()); } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/config/HermesHealthIndicator.java b/src/main/java/de/tum/cit/aet/artemis/communication/config/HermesHealthIndicator.java new file mode 100644 index 000000000000..5e37b8126de1 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/communication/config/HermesHealthIndicator.java @@ -0,0 +1,55 @@ +package de.tum.cit.aet.artemis.communication.config; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + +import java.util.HashMap; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import de.tum.cit.aet.artemis.core.service.connectors.ConnectorHealth; + +/** + * Service determining the health of the Hermes push notification service. + */ +@Profile(PROFILE_CORE) +@Component +public class HermesHealthIndicator implements HealthIndicator { + + private final RestTemplate shortTimeoutRestTemplate; + + @Value("${artemis.push-notification-relay:https://hermes-sandbox.artemis.cit.tum.de}") + private String hermesUrl; + + public HermesHealthIndicator(@Qualifier("shortTimeoutHermesRestTemplate") RestTemplate shortTimeoutRestTemplate) { + this.shortTimeoutRestTemplate = shortTimeoutRestTemplate; + } + + /** + * Ping Hermes and check if the service is available. + */ + @Override + public Health health() { + var additionalInfo = new HashMap(); + additionalInfo.put("url", hermesUrl); + ConnectorHealth health; + try { + ResponseEntity response = shortTimeoutRestTemplate.getForEntity(hermesUrl, String.class); + HttpStatusCode statusCode = response.getStatusCode(); + health = new ConnectorHealth(statusCode.is2xxSuccessful(), additionalInfo); + } + catch (RestClientException error) { + health = new ConnectorHealth(error); + } + + return health.asActuatorHealth(); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/GroupNotificationFactory.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/GroupNotificationFactory.java index 62af1c1ea18a..5c73ce127e07 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/GroupNotificationFactory.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/GroupNotificationFactory.java @@ -242,16 +242,18 @@ public static GroupNotification createAnnouncementNotification(Post post, User a GroupNotification notification; title = NotificationConstants.NEW_ANNOUNCEMENT_POST_TITLE; text = NotificationConstants.NEW_ANNOUNCEMENT_POST_TEXT; + var imageUrl = post.getAuthor().getImageUrl() == null ? "" : post.getAuthor().getImageUrl(); placeholderValues = createPlaceholdersNewAnnouncementPost(course.getTitle(), post.getTitle(), Jsoup.parse(post.getContent()).text(), post.getCreationDate().toString(), - post.getAuthor().getName()); + post.getAuthor().getName(), imageUrl, post.getAuthor().getId().toString(), post.getId().toString()); notification = new GroupNotification(course, title, text, true, placeholderValues, author, groupNotificationType); notification.setTransientAndStringTarget(createCoursePostTarget(post, course)); return notification; } @NotificationPlaceholderCreator(values = { NEW_ANNOUNCEMENT_POST }) - public static String[] createPlaceholdersNewAnnouncementPost(String courseTitle, String postTitle, String postContent, String postCreationDate, String postAuthorName) { - return new String[] { courseTitle, postTitle, postContent, postCreationDate, postAuthorName }; + public static String[] createPlaceholdersNewAnnouncementPost(String courseTitle, String postTitle, String postContent, String postCreationDate, String postAuthorName, + String imageUrl, String authorId, String postId) { + return new String[] { courseTitle, postTitle, postContent, postCreationDate, postAuthorName, imageUrl, authorId, postId }; } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java index 71dc52ae4e93..301cb1bf955b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java @@ -321,9 +321,11 @@ public static SingleUserNotification createNotification(AnswerPost answerPost, N } Conversation conversation = answerPost.getPost().getConversation(); + var imageUrl = answerPost.getAuthor().getImageUrl() != null ? answerPost.getAuthor().getImageUrl() : ""; var placeholders = createPlaceholdersNewReply(conversation.getCourse().getTitle(), answerPost.getPost().getContent(), answerPost.getPost().getCreationDate().toString(), answerPost.getPost().getAuthor().getName(), answerPost.getContent(), answerPost.getCreationDate().toString(), answerPost.getAuthor().getName(), - conversation.getHumanReadableNameForReceiver(answerPost.getAuthor())); + conversation.getHumanReadableNameForReceiver(answerPost.getAuthor()), imageUrl, answerPost.getAuthor().getId().toString(), answerPost.getId().toString(), + answerPost.getPost().getId().toString()); String messageReplyTextType = MESSAGE_REPLY_IN_CONVERSATION_TEXT; @@ -340,8 +342,9 @@ public static SingleUserNotification createNotification(AnswerPost answerPost, N @NotificationPlaceholderCreator(values = { NEW_REPLY_FOR_EXERCISE_POST, NEW_REPLY_FOR_LECTURE_POST, NEW_REPLY_FOR_COURSE_POST, NEW_REPLY_FOR_EXAM_POST, CONVERSATION_NEW_REPLY_MESSAGE, CONVERSATION_USER_MENTIONED }) public static String[] createPlaceholdersNewReply(String courseTitle, String postContent, String postCreationData, String postAuthorName, String answerPostContent, - String answerPostCreationDate, String authorName, String conversationName) { - return new String[] { courseTitle, postContent, postCreationData, postAuthorName, answerPostContent, answerPostCreationDate, authorName, conversationName }; + String answerPostCreationDate, String authorName, String conversationName, String imageUrl, String userId, String postingId, String parentPostId) { + return new String[] { courseTitle, postContent, postCreationData, postAuthorName, answerPostContent, answerPostCreationDate, authorName, conversationName, imageUrl, userId, + postingId, parentPostId }; } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/domain/push_notification/PushNotificationApiType.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/push_notification/PushNotificationApiType.java new file mode 100644 index 000000000000..f191a4a08645 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/push_notification/PushNotificationApiType.java @@ -0,0 +1,23 @@ +package de.tum.cit.aet.artemis.communication.domain.push_notification; + +import java.util.Arrays; + +public enum PushNotificationApiType { + + DEFAULT((short) 0), IOS_V2((short) 1); + + private final short databaseKey; + + PushNotificationApiType(short databaseKey) { + this.databaseKey = databaseKey; + } + + public short getDatabaseKey() { + return databaseKey; + } + + public static PushNotificationApiType fromDatabaseKey(short databaseKey) { + return Arrays.stream(PushNotificationApiType.values()).filter(type -> type.getDatabaseKey() == databaseKey).findFirst() + .orElseThrow(() -> new IllegalArgumentException("Unknown database key: " + databaseKey)); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/domain/push_notification/PushNotificationDeviceConfiguration.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/push_notification/PushNotificationDeviceConfiguration.java index b9a911ce6194..8c0aafae5cea 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/domain/push_notification/PushNotificationDeviceConfiguration.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/push_notification/PushNotificationDeviceConfiguration.java @@ -6,6 +6,7 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.Enumerated; import jakarta.persistence.Id; import jakarta.persistence.IdClass; import jakarta.persistence.JoinColumn; @@ -34,6 +35,10 @@ public class PushNotificationDeviceConfiguration { @Column(name = "device_type") private PushNotificationDeviceType deviceType; + @Enumerated + @Column(name = "api_type") + private PushNotificationApiType apiType; + @Column(name = "expiration_date") private Date expirationDate; @@ -53,6 +58,16 @@ public PushNotificationDeviceConfiguration(String token, PushNotificationDeviceT this.owner = owner; } + public PushNotificationDeviceConfiguration(String token, PushNotificationDeviceType deviceType, Date expirationDate, byte[] secretKey, User owner, + PushNotificationApiType apiType) { + this.token = token; + this.deviceType = deviceType; + this.expirationDate = expirationDate; + this.secretKey = secretKey; + this.owner = owner; + this.apiType = apiType; + } + public PushNotificationDeviceConfiguration() { // needed for JPA } @@ -97,6 +112,10 @@ public void setOwner(User owner) { this.owner = owner; } + public PushNotificationApiType getApiType() { + return apiType; + } + @Override public boolean equals(Object object) { if (this == object) { diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/dto/PushNotificationRegisterBody.java b/src/main/java/de/tum/cit/aet/artemis/communication/dto/PushNotificationRegisterBody.java index 985039d9fba8..7455e2e7d530 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/dto/PushNotificationRegisterBody.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/dto/PushNotificationRegisterBody.java @@ -1,6 +1,11 @@ package de.tum.cit.aet.artemis.communication.dto; +import de.tum.cit.aet.artemis.communication.domain.push_notification.PushNotificationApiType; import de.tum.cit.aet.artemis.communication.domain.push_notification.PushNotificationDeviceType; -public record PushNotificationRegisterBody(String token, PushNotificationDeviceType deviceType) { +public record PushNotificationRegisterBody(String token, PushNotificationDeviceType deviceType, PushNotificationApiType apiType) { + + public PushNotificationRegisterBody(String token, PushNotificationDeviceType deviceType) { + this(token, deviceType, PushNotificationApiType.DEFAULT); + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/conversation/ConversationService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/conversation/ConversationService.java index c2a1761b6b46..1a981ee84f99 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/conversation/ConversationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/conversation/ConversationService.java @@ -143,7 +143,9 @@ public Optional isMemberOrCreateForCourseWideElseThrow(Long conver if (conversation instanceof Channel channel && channel.getIsCourseWide()) { ConversationParticipant conversationParticipant = ConversationParticipant.createWithDefaultValues(user, channel); - conversationParticipant.setIsModerator(authorizationCheckService.isAtLeastInstructorInCourse(channel.getCourse(), user)); + boolean canBecomeModerator = (channel.getIsAnnouncementChannel() ? authorizationCheckService.isAtLeastInstructorInCourse(channel.getCourse(), user) + : authorizationCheckService.isAtLeastTeachingAssistantInCourse(channel.getCourse(), user)); + conversationParticipant.setIsModerator(canBecomeModerator); lastReadDate.ifPresent(conversationParticipant::setLastRead); conversationParticipantRepository.saveAndFlush(conversationParticipant); } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/ConversationNotificationService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/ConversationNotificationService.java index cf5cc2c67cc8..91cc23d2f6f3 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/ConversationNotificationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/ConversationNotificationService.java @@ -84,8 +84,10 @@ public ConversationNotification createNotification(Post createdMessage, Conversa } default -> throw new IllegalStateException("Unexpected value: " + conversation); } + var imageUrl = createdMessage.getAuthor().getImageUrl() == null ? "" : createdMessage.getAuthor().getImageUrl(); String[] placeholders = createPlaceholdersNewMessageChannelText(course.getTitle(), createdMessage.getContent(), createdMessage.getCreationDate().toString(), - conversationName, createdMessage.getAuthor().getName(), conversationType); + conversationName, createdMessage.getAuthor().getName(), conversationType, imageUrl, createdMessage.getAuthor().getId().toString(), + createdMessage.getId().toString()); ConversationNotification notification = createConversationMessageNotification(course.getId(), createdMessage, notificationType, notificationText, true, placeholders); save(notification, mentionedUsers, placeholders, createdMessage); return notification; @@ -93,8 +95,8 @@ public ConversationNotification createNotification(Post createdMessage, Conversa @NotificationPlaceholderCreator(values = { CONVERSATION_NEW_MESSAGE }) public static String[] createPlaceholdersNewMessageChannelText(String courseTitle, String messageContent, String messageCreationDate, String conversationName, - String authorName, String conversationType) { - return new String[] { courseTitle, messageContent, messageCreationDate, conversationName, authorName, conversationType }; + String authorName, String conversationType, String imageUrl, String userId, String postId) { + return new String[] { courseTitle, messageContent, messageCreationDate, conversationName, authorName, conversationType, imageUrl, userId, postId }; } private void save(ConversationNotification notification, Set mentionedUsers, String[] placeHolders, Post createdMessage) { diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/GroupNotificationService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/GroupNotificationService.java index 6e48ec307044..4ee87d9c0173 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/GroupNotificationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/GroupNotificationService.java @@ -120,6 +120,7 @@ private void notifyGroupsWithNotificationType(GroupNotificationType[] groups, No * @param author is the user who initiated the process of the notifications. Can be null if not specified * @param onlySave whether the notification should only be saved and not sent to users */ + @SuppressWarnings("unchecked") private void notifyGroupsWithNotificationType(GroupNotificationType[] groups, NotificationType notificationType, Object notificationSubject, Object typeSpecificInformation, User author, boolean onlySave) { for (GroupNotificationType group : groups) { diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/push_notifications/ApplePushNotificationService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/push_notifications/ApplePushNotificationService.java index ecd0a14f7490..f7a914cff999 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/push_notifications/ApplePushNotificationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/push_notifications/ApplePushNotificationService.java @@ -3,7 +3,6 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import java.util.List; -import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,8 +27,8 @@ public class ApplePushNotificationService extends PushNotificationService { private final PushNotificationDeviceConfigurationRepository repository; - @Value("${artemis.push-notification-relay:#{null}}") - private Optional relayServerBaseUrl; + @Value("${artemis.push-notification-relay:https://hermes-sandbox.artemis.cit.tum.de}") + private String relayServerBaseUrl; public ApplePushNotificationService(PushNotificationDeviceConfigurationRepository repository, RestTemplate restTemplate) { super(restTemplate); @@ -47,7 +46,7 @@ public PushNotificationDeviceType getDeviceType() { } @Override - Optional getRelayBaseUrl() { + String getRelayBaseUrl() { return relayServerBaseUrl; } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/push_notifications/FirebasePushNotificationService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/push_notifications/FirebasePushNotificationService.java index 1d6c843b41a8..c257b7f7fd5a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/push_notifications/FirebasePushNotificationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/push_notifications/FirebasePushNotificationService.java @@ -3,7 +3,6 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import java.util.List; -import java.util.Optional; import java.util.concurrent.CompletableFuture; import org.slf4j.Logger; @@ -32,8 +31,8 @@ public class FirebasePushNotificationService extends PushNotificationService { private final PushNotificationDeviceConfigurationRepository repository; - @Value("${artemis.push-notification-relay:#{null}}") - private Optional relayServerBaseUrl; + @Value("${artemis.push-notification-relay:https://hermes-sandbox.artemis.cit.tum.de}") + private String relayServerBaseUrl; public FirebasePushNotificationService(PushNotificationDeviceConfigurationRepository pushNotificationDeviceConfigurationRepository, RestTemplate restTemplate) { super(restTemplate); @@ -61,7 +60,7 @@ public PushNotificationDeviceType getDeviceType() { } @Override - Optional getRelayBaseUrl() { + String getRelayBaseUrl() { return relayServerBaseUrl; } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/push_notifications/PushNotificationService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/push_notifications/PushNotificationService.java index 3892421894ee..7758e905c06f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/push_notifications/PushNotificationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/push_notifications/PushNotificationService.java @@ -141,7 +141,7 @@ public void sendNotification(Notification notification, User user, Object notifi @Override @Async public void sendNotification(Notification notification, Set users, Object notificationSubject) { - final Optional relayServerBaseUrl = getRelayBaseUrl(); + final String relayServerBaseUrl = getRelayBaseUrl(); if (relayServerBaseUrl.isEmpty()) { return; @@ -155,11 +155,11 @@ public void sendNotification(Notification notification, Set users, Object } final String date = Instant.now().toString(); - var notificationData = new PushNotificationData(notification.getTransientPlaceholderValuesAsArray(), notification.getTarget(), type.name(), date, - Constants.PUSH_NOTIFICATION_VERSION); try { - final String payload = mapper.writeValueAsString(notificationData); + var notificationData = new PushNotificationData(notification.getTransientPlaceholderValuesAsArray(), notification.getTarget(), type.name(), date, + Constants.PUSH_NOTIFICATION_VERSION); + var payload = mapper.writeValueAsString(notificationData); final byte[] initializationVector = new byte[16]; List notificationRequests = userDeviceConfigurations.stream().flatMap(deviceConfiguration -> { @@ -170,10 +170,11 @@ public void sendNotification(Notification notification, Set users, Object String ivAsString = Base64.getEncoder().encodeToString(initializationVector); Optional payloadCiphertext = encrypt(payload, key, initializationVector); - return payloadCiphertext.stream().map(s -> new RelayNotificationRequest(ivAsString, s, deviceConfiguration.getToken())); + return payloadCiphertext.stream() + .map(s -> new RelayNotificationRequest(ivAsString, s, deviceConfiguration.getToken(), deviceConfiguration.getApiType().getDatabaseKey())); }).toList(); - sendNotificationRequestsToEndpoint(notificationRequests, relayServerBaseUrl.get()); + sendNotificationRequestsToEndpoint(notificationRequests, relayServerBaseUrl); } catch (JsonProcessingException e) { log.error("Error creating push notification payload!", e); @@ -184,7 +185,7 @@ public void sendNotification(Notification notification, Set users, Object abstract PushNotificationDeviceType getDeviceType(); - abstract Optional getRelayBaseUrl(); + abstract String getRelayBaseUrl(); abstract String getRelayPath(); diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/push_notifications/RelayNotificationRequest.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/push_notifications/RelayNotificationRequest.java index 3cbe5695ccc8..91f43d2b9088 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/push_notifications/RelayNotificationRequest.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/push_notifications/RelayNotificationRequest.java @@ -1,4 +1,4 @@ package de.tum.cit.aet.artemis.communication.service.notifications.push_notifications; -public record RelayNotificationRequest(String initializationVector, String payloadCipherText, String token) { +public record RelayNotificationRequest(String initializationVector, String payloadCipherText, String token, short apiType) { } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/web/AnswerMessageResource.java b/src/main/java/de/tum/cit/aet/artemis/communication/web/AnswerMessageResource.java index 50f7d2e6f3b8..941a97623f70 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/web/AnswerMessageResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/web/AnswerMessageResource.java @@ -51,7 +51,7 @@ public ResponseEntity createAnswerMessage(@PathVariable Long courseI long start = System.nanoTime(); AnswerPost createdAnswerMessage = answerMessageService.createAnswerMessage(courseId, answerMessage); // creation of answerMessage should not trigger alert - log.info("createAnswerMessage took {}", TimeLogUtil.formatDurationFrom(start)); + log.debug("createAnswerMessage took {}", TimeLogUtil.formatDurationFrom(start)); return ResponseEntity.created(new URI("/api/courses" + courseId + "/answer-messages/" + createdAnswerMessage.getId())).body(createdAnswerMessage); } @@ -70,7 +70,7 @@ public ResponseEntity updateAnswerMessage(@PathVariable Long courseI log.debug("PUT updateAnswerMessage invoked for course {} with message {}", courseId, answerMessage.getContent()); long start = System.nanoTime(); AnswerPost updatedAnswerMessage = answerMessageService.updateAnswerMessage(courseId, answerMessageId, answerMessage); - log.info("updateAnswerMessage took {}", TimeLogUtil.formatDurationFrom(start)); + log.debug("updateAnswerMessage took {}", TimeLogUtil.formatDurationFrom(start)); return new ResponseEntity<>(updatedAnswerMessage, null, HttpStatus.OK); } @@ -88,7 +88,7 @@ public ResponseEntity deleteAnswerMessage(@PathVariable Long courseId, @Pa log.debug("PUT deleteAnswerMessage invoked for course {} on message {}", courseId, answerMessageId); long start = System.nanoTime(); answerMessageService.deleteAnswerMessageById(courseId, answerMessageId); - log.info("deleteAnswerMessage took {}", TimeLogUtil.formatDurationFrom(start)); + log.debug("deleteAnswerMessage took {}", TimeLogUtil.formatDurationFrom(start)); // deletion of answerMessages should not trigger alert return ResponseEntity.ok().build(); } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/web/ConversationMessageResource.java b/src/main/java/de/tum/cit/aet/artemis/communication/web/ConversationMessageResource.java index 75c68fbec7a1..5e0484884a9f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/web/ConversationMessageResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/web/ConversationMessageResource.java @@ -94,7 +94,7 @@ public ResponseEntity createMessage(@PathVariable Long courseId, @Valid @R sendToUserPost.setConversation(sendToUserPost.getConversation().copy()); sendToUserPost.getConversation().setConversationParticipants(Collections.emptySet()); - log.info("createMessage took {}", TimeLogUtil.formatDurationFrom(start)); + log.debug("createMessage took {}", TimeLogUtil.formatDurationFrom(start)); return ResponseEntity.created(new URI("/api/courses/" + courseId + "/messages/" + sendToUserPost.getId())).body(sendToUserPost); } @@ -141,7 +141,7 @@ else if (postContextFilter.courseWideChannelIds() != null) { } private void logDuration(List posts, Principal principal, long timeNanoStart) { - if (log.isInfoEnabled()) { + if (log.isDebugEnabled()) { long answerPosts = posts.stream().mapToLong(post -> post.getAnswers().size()).sum(); long reactions = posts.stream().mapToLong(post -> post.getReactions().size()).sum(); long answerReactions = posts.stream().flatMap(post -> post.getAnswers().stream()).mapToLong(answerPost -> answerPost.getReactions().size()).sum(); @@ -165,7 +165,7 @@ public ResponseEntity updateMessage(@PathVariable Long courseId, @PathVari log.debug("PUT updateMessage invoked for course {} with post {}", courseId, messagePost.getContent()); long start = System.nanoTime(); Post updatedMessagePost = conversationMessagingService.updateMessage(courseId, messageId, messagePost); - log.info("updateMessage took {}", TimeLogUtil.formatDurationFrom(start)); + log.debug("updateMessage took {}", TimeLogUtil.formatDurationFrom(start)); return new ResponseEntity<>(updatedMessagePost, null, HttpStatus.OK); } @@ -184,7 +184,7 @@ public ResponseEntity deleteMessage(@PathVariable Long courseId, @PathVari long start = System.nanoTime(); conversationMessagingService.deleteMessageById(courseId, messageId); // deletion of message posts should not trigger entity deletion alert - log.info("deleteMessage took {}", TimeLogUtil.formatDurationFrom(start)); + log.debug("deleteMessage took {}", TimeLogUtil.formatDurationFrom(start)); return ResponseEntity.ok().build(); } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/web/NotificationResource.java b/src/main/java/de/tum/cit/aet/artemis/communication/web/NotificationResource.java index 911ea5cbfbff..964bdc25e644 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/web/NotificationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/web/NotificationResource.java @@ -75,7 +75,7 @@ public NotificationResource(NotificationRepository notificationRepository, UserR public ResponseEntity> getAllNotificationsForCurrentUserFilteredBySettings(Pageable pageable) { long start = System.nanoTime(); User currentUser = userRepository.getUserWithGroupsAndAuthorities(); - log.info("REST request to get notifications page {} with size {} for current user {} filtered by settings", pageable.getPageNumber(), pageable.getPageSize(), + log.debug("REST request to get notifications page {} with size {} for current user {} filtered by settings", pageable.getPageNumber(), pageable.getPageSize(), currentUser.getLogin()); var tutorialGroupIds = tutorialGroupService.findAllForNotifications(currentUser); var notificationSettings = notificationSettingRepository.findAllNotificationSettingsForRecipientWithId(currentUser.getId()); @@ -97,7 +97,7 @@ public ResponseEntity> getAllNotificationsForCurrentUserFilte deactivatedTitles, tutorialGroupIds, TITLES_TO_NOT_LOAD_NOTIFICATION, pageable); } HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); - log.info("Load notifications for user {} done in {}", currentUser.getLogin(), TimeLogUtil.formatDurationFrom(start)); + log.debug("Load notifications for user {} done in {}", currentUser.getLogin(), TimeLogUtil.formatDurationFrom(start)); return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/web/PushNotificationResource.java b/src/main/java/de/tum/cit/aet/artemis/communication/web/PushNotificationResource.java index f8ccde45eeb6..3078e9f3d6cd 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/web/PushNotificationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/web/PushNotificationResource.java @@ -24,6 +24,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import de.tum.cit.aet.artemis.communication.domain.push_notification.PushNotificationApiType; import de.tum.cit.aet.artemis.communication.domain.push_notification.PushNotificationDeviceConfiguration; import de.tum.cit.aet.artemis.communication.domain.push_notification.PushNotificationDeviceConfigurationId; import de.tum.cit.aet.artemis.communication.dto.PushNotificationRegisterBody; @@ -100,10 +101,12 @@ public ResponseEntity register(@Valid @RequestBody return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } + PushNotificationApiType apiType = pushNotificationRegisterBody.apiType() != null ? pushNotificationRegisterBody.apiType() : PushNotificationApiType.DEFAULT; + User user = userRepository.getUser(); PushNotificationDeviceConfiguration deviceConfiguration = new PushNotificationDeviceConfiguration(pushNotificationRegisterBody.token(), - pushNotificationRegisterBody.deviceType(), expirationDate, newKey.getEncoded(), user); + pushNotificationRegisterBody.deviceType(), expirationDate, newKey.getEncoded(), user, apiType); pushNotificationDeviceConfigurationRepository.save(deviceConfiguration); var encodedKey = Base64.getEncoder().encodeToString(newKey.getEncoded()); @@ -120,13 +123,13 @@ public ResponseEntity register(@Valid @RequestBody @DeleteMapping("unregister") @EnforceAtLeastStudent public ResponseEntity unregister(@Valid @RequestBody PushNotificationUnregisterRequest body) { - final var id = new PushNotificationDeviceConfigurationId(userRepository.getUser(), body.token(), body.deviceType()); + final var deviceId = new PushNotificationDeviceConfigurationId(userRepository.getUser(), body.token(), body.deviceType()); - if (!pushNotificationDeviceConfigurationRepository.existsById(id)) { + if (!pushNotificationDeviceConfigurationRepository.existsById(deviceId)) { return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); } - pushNotificationDeviceConfigurationRepository.deleteById(id); + pushNotificationDeviceConfigurationRepository.deleteById(deviceId); return ResponseEntity.ok().build(); } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/Constants.java b/src/main/java/de/tum/cit/aet/artemis/core/config/Constants.java index 6ddd70dad841..843a9034d46c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/config/Constants.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/Constants.java @@ -39,6 +39,8 @@ public final class Constants { public static final int QUIZ_GRACE_PERIOD_IN_SECONDS = 5; + public static final int MAX_ENVIRONMENT_VARIABLES_DOCKER_FLAG_LENGTH = 1000; + /** * This constant determines how many seconds after the exercise due dates submissions will still be considered rated. * Submissions after the grace period exceeded will be flagged as illegal. @@ -376,6 +378,11 @@ public final class Constants { */ public static final int PUSH_NOTIFICATION_VERSION = 1; + /** + * The value of the version field we send with each push notification to the native clients (Android & iOS). + */ + public static final int PUSH_NOTIFICATION_MINOR_VERSION = 2; + /** * The directory in the docker container in which the build script is executed */ diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/EurekaClientRestTemplateConfiguration.java b/src/main/java/de/tum/cit/aet/artemis/core/config/EurekaClientConfiguration.java similarity index 52% rename from src/main/java/de/tum/cit/aet/artemis/core/config/EurekaClientRestTemplateConfiguration.java rename to src/main/java/de/tum/cit/aet/artemis/core/config/EurekaClientConfiguration.java index b66038d20469..26634548eb3b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/config/EurekaClientRestTemplateConfiguration.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/EurekaClientConfiguration.java @@ -8,38 +8,42 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.cloud.configuration.SSLContextFactory; import org.springframework.cloud.configuration.TlsProperties; +import org.springframework.cloud.netflix.eureka.RestClientTimeoutProperties; +import org.springframework.cloud.netflix.eureka.http.DefaultEurekaClientHttpRequestFactorySupplier; import org.springframework.cloud.netflix.eureka.http.EurekaClientHttpRequestFactorySupplier; -import org.springframework.cloud.netflix.eureka.http.RestTemplateDiscoveryClientOptionalArgs; -import org.springframework.cloud.netflix.eureka.http.RestTemplateTransportClientFactories; +import org.springframework.cloud.netflix.eureka.http.RestClientDiscoveryClientOptionalArgs; +import org.springframework.cloud.netflix.eureka.http.RestClientTransportClientFactories; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; +import org.springframework.web.client.RestClient; /** * This class is necessary to avoid using Jersey (which has an issue deserializing Eureka responses) after the spring boot upgrade. - * It provides the RestTemplateTransportClientFactories and RestTemplateDiscoveryClientOptionalArgs that would normally not be instantiated + * It provides the RestClientTransportClientFactories and RestClientDiscoveryClientOptionalArgs that would normally not be instantiated * when Jersey is found by Eureka. */ @Profile({ PROFILE_CORE, PROFILE_BUILDAGENT }) @Configuration -public class EurekaClientRestTemplateConfiguration { +public class EurekaClientConfiguration { - private static final Logger log = LoggerFactory.getLogger(EurekaClientRestTemplateConfiguration.class); + private static final Logger log = LoggerFactory.getLogger(EurekaClientConfiguration.class); /** - * Configures and returns {@link RestTemplateDiscoveryClientOptionalArgs} for Eureka client communication, + * Configures and returns {@link RestClientDiscoveryClientOptionalArgs} for Eureka client communication, * with optional TLS/SSL setup based on provided configuration. *

- * This method leverages the {@link EurekaClientHttpRequestFactorySupplier} to configure the RestTemplate + * This method leverages the {@link EurekaClientHttpRequestFactorySupplier} to configure the RestClient * specifically for Eureka client interactions. If TLS is enabled in the provided {@link TlsProperties}, * a custom SSLContext is set up to ensure secure communication. *

* - * @param tlsProperties The TLS configuration properties, used to check if TLS is enabled and to configure it accordingly. - * @param eurekaClientHttpRequestFactorySupplier Supplies the HTTP request factory for the Eureka client RestTemplate. - * @return A configured instance of {@link RestTemplateDiscoveryClientOptionalArgs} for Eureka client, + * @param tlsProperties The TLS configuration properties, used to check if TLS is enabled and to configure it accordingly. + * @param restClientBuilderProvider The provider for the {@link RestClient.Builder} instance, if available. + * @return A configured instance of {@link RestClientDiscoveryClientOptionalArgs} for Eureka client, * potentially with SSL/TLS enabled if specified in the {@code tlsProperties}. * @throws GeneralSecurityException If there's an issue with setting up the SSL/TLS context. * @throws IOException If there's an I/O error during the setup. @@ -47,12 +51,13 @@ public class EurekaClientRestTemplateConfiguration { * @see EurekaClientHttpRequestFactorySupplier */ @Bean - public RestTemplateDiscoveryClientOptionalArgs restTemplateDiscoveryClientOptionalArgs(TlsProperties tlsProperties, - EurekaClientHttpRequestFactorySupplier eurekaClientHttpRequestFactorySupplier) throws GeneralSecurityException, IOException { - log.debug("Using RestTemplate for the Eureka client."); + public RestClientDiscoveryClientOptionalArgs restClientDiscoveryClientOptionalArgs(TlsProperties tlsProperties, ObjectProvider restClientBuilderProvider) + throws GeneralSecurityException, IOException { + log.debug("Using RestClient for the Eureka client."); // The Eureka DiscoveryClientOptionalArgsConfiguration invokes a private method setupTLS. // This code is taken from that method. - var args = new RestTemplateDiscoveryClientOptionalArgs(eurekaClientHttpRequestFactorySupplier); + var supplier = new DefaultEurekaClientHttpRequestFactorySupplier(new RestClientTimeoutProperties()); + var args = new RestClientDiscoveryClientOptionalArgs(supplier, () -> restClientBuilderProvider.getIfAvailable(RestClient::builder)); if (tlsProperties.isEnabled()) { SSLContextFactory factory = new SSLContextFactory(tlsProperties); args.setSSLContext(factory.createSSLContext()); @@ -61,7 +66,7 @@ public RestTemplateDiscoveryClientOptionalArgs restTemplateDiscoveryClientOption } @Bean - public RestTemplateTransportClientFactories restTemplateTransportClientFactories(RestTemplateDiscoveryClientOptionalArgs optionalArgs) { - return new RestTemplateTransportClientFactories(optionalArgs); + public RestClientTransportClientFactories restClientTransportClientFactories(RestClientDiscoveryClientOptionalArgs optionalArgs) { + return new RestClientTransportClientFactories(optionalArgs); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/LiquibaseConfiguration.java b/src/main/java/de/tum/cit/aet/artemis/core/config/LiquibaseConfiguration.java index 5aa0b02bcc36..5a0847aeedcb 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/config/LiquibaseConfiguration.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/LiquibaseConfiguration.java @@ -75,14 +75,14 @@ public SpringLiquibase liquibase(@LiquibaseDataSource ObjectProvider SpringLiquibase liquibase = SpringLiquibaseUtil.createSpringLiquibase(liquibaseDataSource.getIfAvailable(), liquibaseProperties, dataSource, dataSourceProperties); Scope.setScopeManager(new SingletonScopeManager()); liquibase.setChangeLog("classpath:config/liquibase/master.xml"); - liquibase.setContexts(liquibaseProperties.getContexts()); + liquibase.setContexts(liquibaseProperties.getContexts() != null ? liquibaseProperties.getContexts().getFirst() : null); liquibase.setDefaultSchema(liquibaseProperties.getDefaultSchema()); liquibase.setLiquibaseSchema(liquibaseProperties.getLiquibaseSchema()); liquibase.setLiquibaseTablespace(liquibaseProperties.getLiquibaseTablespace()); liquibase.setDatabaseChangeLogLockTable(liquibaseProperties.getDatabaseChangeLogLockTable()); liquibase.setDatabaseChangeLogTable(liquibaseProperties.getDatabaseChangeLogTable()); liquibase.setDropFirst(liquibaseProperties.isDropFirst()); - liquibase.setLabelFilter(liquibaseProperties.getLabelFilter()); + liquibase.setLabelFilter(liquibaseProperties.getLabelFilter() != null ? liquibaseProperties.getLabelFilter().getFirst() : null); liquibase.setChangeLogParameters(liquibaseProperties.getParameters()); liquibase.setRollbackFile(liquibaseProperties.getRollbackFile()); liquibase.setTestRollbackOnUpdate(liquibaseProperties.isTestRollbackOnUpdate()); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/RestTemplateConfiguration.java b/src/main/java/de/tum/cit/aet/artemis/core/config/RestTemplateConfiguration.java index 0730267fb379..4ba55bc3c7dd 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/config/RestTemplateConfiguration.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/RestTemplateConfiguration.java @@ -9,7 +9,6 @@ import jakarta.validation.constraints.NotNull; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; @@ -43,14 +42,12 @@ public class RestTemplateConfiguration { @Bean @Profile("gitlab | gitlabci") - @Autowired // ok public RestTemplate gitlabRestTemplate(GitLabAuthorizationInterceptor gitlabInterceptor) { return initializeRestTemplateWithInterceptors(gitlabInterceptor, createRestTemplate()); } @Bean @Profile("jenkins") - @Autowired // ok public RestTemplate jenkinsRestTemplate(JenkinsAuthorizationInterceptor jenkinsInterceptor) { return initializeRestTemplateWithInterceptors(jenkinsInterceptor, createRestTemplate()); } @@ -89,14 +86,12 @@ public RestTemplate pyrisRestTemplate(PyrisAuthorizationInterceptor pyrisAuthori @Bean @Profile("gitlab | gitlabci") - @Autowired // ok public RestTemplate shortTimeoutGitlabRestTemplate(GitLabAuthorizationInterceptor gitlabInterceptor) { return initializeRestTemplateWithInterceptors(gitlabInterceptor, createShortTimeoutRestTemplate()); } @Bean @Profile("jenkins") - @Autowired // ok public RestTemplate shortTimeoutJenkinsRestTemplate(JenkinsAuthorizationInterceptor jenkinsInterceptor) { return initializeRestTemplateWithInterceptors(jenkinsInterceptor, createShortTimeoutRestTemplate()); } @@ -113,6 +108,11 @@ public RestTemplate shortTimeoutApollonRestTemplate() { return createShortTimeoutRestTemplate(); } + @Bean + public RestTemplate shortTimeoutHermesRestTemplate() { + return createShortTimeoutRestTemplate(); + } + // Note: for certain requests, e.g. the Athena submission selection, we would like to have even shorter timeouts. // Therefore, we need additional rest templates. It is recommended to keep the timeout settings constant per rest template. diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/StompErrorLogFilter.java b/src/main/java/de/tum/cit/aet/artemis/core/config/StompErrorLogFilter.java new file mode 100644 index 000000000000..c71cbac05f6d --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/StompErrorLogFilter.java @@ -0,0 +1,55 @@ +package de.tum.cit.aet.artemis.core.config; + +import org.slf4j.Marker; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.turbo.TurboFilter; +import ch.qos.logback.core.spi.FilterReply; + +/** + * A custom Logback filter to suppress specific log messages from the + * StompBrokerRelayMessageHandler in a Spring Boot application. + * + *

+ * This filter identifies log messages containing the error: + * "Did not receive data from ... within the 60000ms connection TTL. The connection will now be closed." + * and suppresses them while allowing other log messages to pass through. + * + *

+ * The purpose of this filter is to reduce noise in the logs by eliminating + * repetitive or irrelevant error messages caused by client disconnections. + */ +public class StompErrorLogFilter extends TurboFilter { + + /** + * Determines whether a log message should be allowed, denied, or processed normally. + * + *

+ * This method checks the logger name, log level, and message format to identify + * and suppress specific error messages. If the message matches the criteria + * (e.g., the logger is from {@code StompBrokerRelayMessageHandler} and the error + * contains details about a 60000ms connection TTL timeout), the log is denied. + * Otherwise, the log message is processed normally. + * + * @param marker The marker associated with the log message (can be null). + * @param logger The logger that created the log message. + * @param level The log level (e.g., ERROR, WARN, INFO). + * @param format The log message format string. + * @param params Parameters for the format string (if any). + * @param t Throwable associated with the log event (if any). + * @return {@link FilterReply#DENY} if the message matches the suppression criteria, + * otherwise {@link FilterReply#NEUTRAL} to process the message normally. + */ + @Override + public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) { + + // Check if the logger and message match the specific error to suppress + if ("org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler".equals(logger.getName()) && Level.ERROR.equals(level) && format != null + && format.contains("Did not receive data from") && format.contains("connection TTL. The connection will now be closed.")) { + return FilterReply.DENY; // Suppress this specific log message + } + + return FilterReply.NEUTRAL; // Allow other messages + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java b/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java index d0c6941cc698..4a5dfdacfa24 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java @@ -19,7 +19,6 @@ import java.util.regex.Pattern; import jakarta.annotation.Nullable; -import jakarta.servlet.http.Cookie; import jakarta.validation.constraints.NotNull; import org.slf4j.Logger; @@ -28,6 +27,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpStatusCode; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; @@ -52,7 +52,6 @@ import org.springframework.web.socket.server.HandshakeInterceptor; import org.springframework.web.socket.server.support.DefaultHandshakeHandler; import org.springframework.web.socket.sockjs.transport.handler.WebSocketTransportHandler; -import org.springframework.web.util.WebUtils; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.Iterators; @@ -201,9 +200,14 @@ public HandshakeInterceptor httpSessionHandshakeInterceptor() { public boolean beforeHandshake(@NotNull ServerHttpRequest request, @NotNull ServerHttpResponse response, @NotNull WebSocketHandler wsHandler, @NotNull Map attributes) { if (request instanceof ServletServerHttpRequest servletRequest) { - attributes.put(IP_ADDRESS, servletRequest.getRemoteAddress()); - Cookie jwtCookie = WebUtils.getCookie(servletRequest.getServletRequest(), JWTFilter.JWT_COOKIE_NAME); - return JWTFilter.isJwtCookieValid(tokenProvider, jwtCookie); + try { + attributes.put(IP_ADDRESS, servletRequest.getRemoteAddress()); + return JWTFilter.extractValidJwt(servletRequest.getServletRequest(), tokenProvider) != null; + } + catch (IllegalArgumentException e) { + response.setStatusCode(HttpStatusCode.valueOf(400)); + return false; + } } return false; } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java index a7373fcd9874..ff1ddcaaf3e3 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java @@ -2,12 +2,14 @@ import java.io.IOException; +import jakarta.annotation.Nullable; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -22,6 +24,10 @@ public class JWTFilter extends GenericFilterBean { public static final String JWT_COOKIE_NAME = "jwt"; + private static final String AUTHORIZATION_HEADER = "Authorization"; + + private static final String BEARER_PREFIX = "Bearer "; + private final TokenProvider tokenProvider; public JWTFilter(TokenProvider tokenProvider) { @@ -31,26 +37,89 @@ public JWTFilter(TokenProvider tokenProvider) { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; - Cookie jwtCookie = WebUtils.getCookie(httpServletRequest, JWT_COOKIE_NAME); - if (isJwtCookieValid(this.tokenProvider, jwtCookie)) { - Authentication authentication = this.tokenProvider.getAuthentication(jwtCookie.getValue()); + HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse; + String jwtToken; + try { + jwtToken = extractValidJwt(httpServletRequest, this.tokenProvider); + } + catch (IllegalArgumentException e) { + httpServletResponse.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + + if (jwtToken != null) { + Authentication authentication = this.tokenProvider.getAuthentication(jwtToken); SecurityContextHolder.getContext().setAuthentication(authentication); } + filterChain.doFilter(servletRequest, servletResponse); } /** - * Checks if the cookie containing the jwt is valid + * Extracts the valid jwt found in the cookie or the Authorization header * - * @param tokenProvider the artemis token provider used to generate and validate jwt's - * @param jwtCookie the cookie containing the jwt - * @return true if the jwt is valid, false if missing or invalid + * @param httpServletRequest the http request + * @param tokenProvider the Artemis token provider used to generate and validate jwt's + * @return the valid jwt or null if not found or invalid */ - public static boolean isJwtCookieValid(TokenProvider tokenProvider, Cookie jwtCookie) { + public static @Nullable String extractValidJwt(HttpServletRequest httpServletRequest, TokenProvider tokenProvider) { + var cookie = WebUtils.getCookie(httpServletRequest, JWT_COOKIE_NAME); + var authHeader = httpServletRequest.getHeader(AUTHORIZATION_HEADER); + + if (cookie == null && authHeader == null) { + return null; + } + + if (cookie != null && authHeader != null) { + // Single Method Enforcement: Only one method of authentication is allowed + throw new IllegalArgumentException("Multiple authentication methods detected: Both JWT cookie and Bearer token are present"); + } + + String jwtToken = cookie != null ? getJwtFromCookie(cookie) : getJwtFromBearer(authHeader); + + if (!isJwtValid(tokenProvider, jwtToken)) { + return null; + } + + return jwtToken; + } + + /** + * Extracts the jwt from the cookie + * + * @param jwtCookie the cookie with Key "jwt" + * @return the jwt or null if not found + */ + private static @Nullable String getJwtFromCookie(@Nullable Cookie jwtCookie) { if (jwtCookie == null) { - return false; + return null; + } + return jwtCookie.getValue(); + } + + /** + * Extracts the jwt from the Authorization header + * + * @param jwtBearer the content of the Authorization header + * @return the jwt or null if not found + */ + private static @Nullable String getJwtFromBearer(@Nullable String jwtBearer) { + if (!StringUtils.hasText(jwtBearer) || !jwtBearer.startsWith(BEARER_PREFIX)) { + return null; } - String jwt = jwtCookie.getValue(); - return StringUtils.hasText(jwt) && tokenProvider.validateTokenForAuthority(jwt); + + String token = jwtBearer.substring(BEARER_PREFIX.length()).trim(); + return StringUtils.hasText(token) ? token : null; + } + + /** + * Checks if the jwt is valid + * + * @param tokenProvider the Artemis token provider used to generate and validate jwt's + * @param jwtToken the jwt + * @return true if the jwt is valid, false if missing or invalid + */ + private static boolean isJwtValid(TokenProvider tokenProvider, @Nullable String jwtToken) { + return StringUtils.hasText(jwtToken) && tokenProvider.validateTokenForAuthority(jwtToken); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java index 262ece79700d..044d897d12c7 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java @@ -170,6 +170,11 @@ private Claims parseClaims(String authToken) { return Jwts.parser().verifyWith(key).build().parseSignedClaims(authToken).getPayload(); } + public T getClaim(String token, String claimName, Class claimType) { + Claims claims = parseClaims(token); + return claims.get(claimName, claimType); + } + public Date getExpirationDate(String authToken) { return parseClaims(authToken).getExpiration(); } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/messaging/InstanceMessageReceiveService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/messaging/InstanceMessageReceiveService.java index cb1566339db9..ef85f1d650fe 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/messaging/InstanceMessageReceiveService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/messaging/InstanceMessageReceiveService.java @@ -337,7 +337,7 @@ public void processStudentExamIndividualWorkingTimeChangeDuringConduction(Long s } public void processScheduleParticipantScore(Long exerciseId, Long participantId, Long resultIdToBeDeleted) { - log.info("Received schedule participant score for exercise {} and participant {} (result to be deleted: {})", exerciseId, participantId, resultIdToBeDeleted); + log.debug("Received schedule participant score for exercise {} and participant {} (result to be deleted: {})", exerciseId, participantId, resultIdToBeDeleted); participantScoreScheduleService.scheduleTask(exerciseId, participantId, resultIdToBeDeleted); } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/FileResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/FileResource.java index 27332e5343a7..11c5d1278609 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/FileResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/FileResource.java @@ -1,6 +1,8 @@ package de.tum.cit.aet.artemis.core.web; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; +import static org.apache.velocity.shaded.commons.io.FilenameUtils.getBaseName; +import static org.apache.velocity.shaded.commons.io.FilenameUtils.getExtension; import java.io.IOException; import java.net.FileNameMap; @@ -409,20 +411,18 @@ public ResponseEntity getExamUserImage(@PathVariable Long examUserId) { /** * GET /files/attachments/lecture/:lectureId/:filename : Get the lecture attachment * - * @param lectureId ID of the lecture, the attachment belongs to - * @param filename the filename of the file + * @param lectureId ID of the lecture, the attachment belongs to + * @param attachmentName the filename of the file * @return The requested file, 403 if the logged-in user is not allowed to access it, or 404 if the file doesn't exist */ - @GetMapping("files/attachments/lecture/{lectureId}/{filename}") + @GetMapping("files/attachments/lecture/{lectureId}/{attachmentName}") @EnforceAtLeastStudent - public ResponseEntity getLectureAttachment(@PathVariable Long lectureId, @PathVariable String filename) { - log.debug("REST request to get lecture attachment : {}", filename); - String fileNameWithoutSpaces = filename.replaceAll(" ", "_"); - sanitizeFilenameElseThrow(fileNameWithoutSpaces); + public ResponseEntity getLectureAttachment(@PathVariable Long lectureId, @PathVariable String attachmentName) { + log.debug("REST request to get lecture attachment : {}", attachmentName); List lectureAttachments = attachmentRepository.findAllByLectureId(lectureId); - Attachment attachment = lectureAttachments.stream().filter(lectureAttachment -> lectureAttachment.getLink().endsWith(fileNameWithoutSpaces)).findAny() - .orElseThrow(() -> new EntityNotFoundException("Attachment", filename)); + Attachment attachment = lectureAttachments.stream().filter(lectureAttachment -> lectureAttachment.getName().equals(getBaseName(attachmentName))).findAny() + .orElseThrow(() -> new EntityNotFoundException("Attachment", attachmentName)); // get the course for a lecture attachment Lecture lecture = attachment.getLecture(); @@ -431,7 +431,7 @@ public ResponseEntity getLectureAttachment(@PathVariable Long lectureId, // check if the user is authorized to access the requested attachment unit checkAttachmentAuthorizationOrThrow(course, attachment); - return buildFileResponse(getActualPathFromPublicPathString(attachment.getLink()), Optional.of(attachment.getName())); + return buildFileResponse(getActualPathFromPublicPathString(attachment.getLink()), Optional.of(attachmentName)); } /** @@ -487,7 +487,7 @@ public ResponseEntity getAttachmentUnitAttachment(@PathVariable Long att // check if the user is authorized to access the requested attachment unit checkAttachmentAuthorizationOrThrow(course, attachment); - return buildFileResponse(getActualPathFromPublicPathString(attachment.getLink()), Optional.of(attachment.getName())); + return buildFileResponse(getActualPathFromPublicPathString(attachment.getLink()), Optional.of(attachment.getName() + "." + getExtension(attachment.getLink()))); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicAccountResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicAccountResource.java index 543ad85964da..3dbea43bf367 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicAccountResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicAccountResource.java @@ -166,7 +166,7 @@ public ResponseEntity getAccount() { // we set this value on purpose here: the user can only fetch their own information, make the token available for constructing the token-based clone-URL userDTO.setVcsAccessToken(user.getVcsAccessToken()); userDTO.setVcsAccessTokenExpiryDate(user.getVcsAccessTokenExpiryDate()); - log.info("GET /account {} took {}ms", user.getLogin(), System.currentTimeMillis() - start); + log.debug("GET /account {} took {}ms", user.getLogin(), System.currentTimeMillis() - start); return ResponseEntity.ok(userDTO); } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java index 44e44a0ff87a..90020572f571 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java @@ -2,6 +2,7 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; +import java.util.Map; import java.util.Optional; import jakarta.servlet.ServletException; @@ -69,7 +70,7 @@ public PublicUserJwtResource(JWTCookieService jwtCookieService, AuthenticationMa */ @PostMapping("authenticate") @EnforceNothing - public ResponseEntity authorize(@Valid @RequestBody LoginVM loginVM, @RequestHeader("User-Agent") String userAgent, HttpServletResponse response) { + public ResponseEntity> authorize(@Valid @RequestBody LoginVM loginVM, @RequestHeader("User-Agent") String userAgent, HttpServletResponse response) { var username = loginVM.getUsername(); var password = loginVM.getPassword(); @@ -86,7 +87,7 @@ public ResponseEntity authorize(@Valid @RequestBody LoginVM loginVM, @Requ ResponseCookie responseCookie = jwtCookieService.buildLoginCookie(rememberMe); response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString()); - return ResponseEntity.ok().build(); + return ResponseEntity.ok(Map.of("access_token", responseCookie.getValue())); } catch (BadCredentialsException ex) { log.warn("Wrong credentials during login for user {}", loginVM.getUsername()); diff --git a/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamService.java b/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamService.java index 40184805ade5..fe337ea23d5f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamService.java +++ b/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamService.java @@ -1421,17 +1421,17 @@ public void cleanupExam(Long examId, Principal principal) { * considering the use of Compass. * * @param exam The exam entity for which the student exams and exercises need to be updated and rescheduled. The student exams must be already loaded. - * @param originalExamDuration The original duration of the exam, in minutes, before any changes. - * @param workingTimeChange The amount of time, in minutes, to add or subtract from the exam's original duration and the student's working time. This value can be positive + * @param originalExamDuration The original duration of the exam, in seconds, before any changes. + * @param workingTimeChange The amount of time, in seconds, to add or subtract from the exam's original duration and the student's working time. This value can be positive * (to extend time) or negative (to reduce time). */ - public void updateStudentExamsAndRescheduleExercises(Exam exam, Integer originalExamDuration, Integer workingTimeChange) { + public void updateStudentExamsAndRescheduleExercises(Exam exam, int originalExamDuration, int workingTimeChange) { var now = now(); User instructor = userRepository.getUser(); var studentExams = exam.getStudentExams(); for (var studentExam : studentExams) { - Integer originalStudentWorkingTime = studentExam.getWorkingTime(); + int originalStudentWorkingTime = studentExam.getWorkingTime(); int originalTimeExtension = originalStudentWorkingTime - originalExamDuration; // NOTE: take the original working time extensions into account if (originalTimeExtension == 0) { diff --git a/src/main/java/de/tum/cit/aet/artemis/exam/web/ExamResource.java b/src/main/java/de/tum/cit/aet/artemis/exam/web/ExamResource.java index 8223ba8e54a9..ddebe7b74068 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exam/web/ExamResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/exam/web/ExamResource.java @@ -313,7 +313,7 @@ public ResponseEntity updateExam(@PathVariable Long courseId, @RequestBody */ @PatchMapping("courses/{courseId}/exams/{examId}/working-time") @EnforceAtLeastInstructor - public ResponseEntity updateExamWorkingTime(@PathVariable Long courseId, @PathVariable Long examId, @RequestBody Integer workingTimeChange) { + public ResponseEntity updateExamWorkingTime(@PathVariable Long courseId, @PathVariable Long examId, @RequestBody int workingTimeChange) { log.debug("REST request to update the working time of exam with id {}", examId); examAccessService.checkCourseAndExamAccessForInstructorElseThrow(courseId, examId); diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisCourseChatSubSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisCourseChatSubSettings.java new file mode 100644 index 000000000000..7428f7feb3b3 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisCourseChatSubSettings.java @@ -0,0 +1,44 @@ +package de.tum.cit.aet.artemis.iris.domain.settings; + +import jakarta.annotation.Nullable; +import jakarta.persistence.Column; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * An {@link IrisSubSettings} implementation for course chat settings. + * Chat settings notably provide settings for the rate limit. + */ +@Entity +@DiscriminatorValue("COURSE_CHAT") +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class IrisCourseChatSubSettings extends IrisSubSettings { + + @Nullable + @Column(name = "rate_limit") + private Integer rateLimit; + + @Nullable + @Column(name = "rate_limit_timeframe_hours") + private Integer rateLimitTimeframeHours; + + @Nullable + public Integer getRateLimit() { + return rateLimit; + } + + public void setRateLimit(@Nullable Integer rateLimit) { + this.rateLimit = rateLimit; + } + + @Nullable + public Integer getRateLimitTimeframeHours() { + return rateLimitTimeframeHours; + } + + public void setRateLimitTimeframeHours(@Nullable Integer rateLimitTimeframeHours) { + this.rateLimitTimeframeHours = rateLimitTimeframeHours; + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisCourseSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisCourseSettings.java index fce389a7b95f..8320f2b6d708 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisCourseSettings.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisCourseSettings.java @@ -32,6 +32,10 @@ public class IrisCourseSettings extends IrisSettings { @JoinColumn(name = "iris_text_exercise_chat_settings_id") private IrisTextExerciseChatSubSettings irisTextExerciseChatSettings; + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, optional = false) + @JoinColumn(name = "iris_course_chat_settings_id") + private IrisCourseChatSubSettings irisCourseChatSettings; + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) @JoinColumn(name = "iris_lecture_ingestion_settings_id") private IrisLectureIngestionSubSettings irisLectureIngestionSettings; @@ -78,6 +82,16 @@ public void setIrisTextExerciseChatSettings(IrisTextExerciseChatSubSettings iris this.irisTextExerciseChatSettings = irisTextExerciseChatSettings; } + @Override + public IrisCourseChatSubSettings getIrisCourseChatSettings() { + return irisCourseChatSettings; + } + + @Override + public void setIrisCourseChatSettings(IrisCourseChatSubSettings irisCourseChatSettings) { + this.irisCourseChatSettings = irisCourseChatSettings; + } + @Override public IrisCompetencyGenerationSubSettings getIrisCompetencyGenerationSettings() { return irisCompetencyGenerationSettings; diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisExerciseSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisExerciseSettings.java index ba095a018808..8048a76e976b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisExerciseSettings.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisExerciseSettings.java @@ -69,6 +69,17 @@ public void setIrisTextExerciseChatSettings(IrisTextExerciseChatSubSettings iris this.irisTextExerciseChatSettings = irisTextExerciseChatSettings; } + @Override + public IrisCourseChatSubSettings getIrisCourseChatSettings() { + // Empty because exercises don't have course chat settings + return null; + } + + @Override + public void setIrisCourseChatSettings(IrisCourseChatSubSettings irisCourseChatSettings) { + // Empty because exercises don't have course chat settings + } + @Override public IrisCompetencyGenerationSubSettings getIrisCompetencyGenerationSettings() { return null; diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisGlobalSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisGlobalSettings.java index ddb156da0038..5531f65584ff 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisGlobalSettings.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisGlobalSettings.java @@ -27,6 +27,10 @@ public class IrisGlobalSettings extends IrisSettings { @JoinColumn(name = "iris_text_exercise_chat_settings_id") private IrisTextExerciseChatSubSettings irisTextExerciseChatSettings; + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, optional = false) + @JoinColumn(name = "iris_course_chat_settings_id") + private IrisCourseChatSubSettings irisCourseChatSettings; + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, optional = false) @JoinColumn(name = "iris_lecture_ingestion_settings_id") private IrisLectureIngestionSubSettings irisLectureIngestionSettings; @@ -65,6 +69,16 @@ public void setIrisTextExerciseChatSettings(IrisTextExerciseChatSubSettings iris this.irisTextExerciseChatSettings = irisTextExerciseChatSettings; } + @Override + public IrisCourseChatSubSettings getIrisCourseChatSettings() { + return irisCourseChatSettings; + } + + @Override + public void setIrisCourseChatSettings(IrisCourseChatSubSettings irisCourseChatSettings) { + this.irisCourseChatSettings = irisCourseChatSettings; + } + @Override public IrisCompetencyGenerationSubSettings getIrisCompetencyGenerationSettings() { return irisCompetencyGenerationSettings; diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSettings.java index 61b2912d5cf6..d67d49caeab0 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSettings.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSettings.java @@ -49,6 +49,10 @@ public abstract class IrisSettings extends DomainObject { public abstract void setIrisTextExerciseChatSettings(IrisTextExerciseChatSubSettings irisTextExerciseChatSettings); + public abstract IrisCourseChatSubSettings getIrisCourseChatSettings(); + + public abstract void setIrisCourseChatSettings(IrisCourseChatSubSettings irisCourseChatSettings); + public abstract IrisLectureIngestionSubSettings getIrisLectureIngestionSettings(); public abstract void setIrisLectureIngestionSettings(IrisLectureIngestionSubSettings irisLectureIngestionSettings); diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSubSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSubSettings.java index c9fc576311db..86e77fc9c034 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSubSettings.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSubSettings.java @@ -40,6 +40,7 @@ @JsonSubTypes({ @JsonSubTypes.Type(value = IrisChatSubSettings.class, name = "chat"), @JsonSubTypes.Type(value = IrisTextExerciseChatSubSettings.class, name = "text-exercise-chat"), + @JsonSubTypes.Type(value = IrisCourseChatSubSettings.class, name = "course-chat"), @JsonSubTypes.Type(value = IrisLectureIngestionSubSettings.class, name = "lecture-ingestion"), @JsonSubTypes.Type(value = IrisCompetencyGenerationSubSettings.class, name = "competency-generation") }) diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSubSettingsType.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSubSettingsType.java index dafdd1edcfb9..fe3561f12c2a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSubSettingsType.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSubSettingsType.java @@ -1,6 +1,6 @@ package de.tum.cit.aet.artemis.iris.domain.settings; public enum IrisSubSettingsType { - CHAT, // TODO: Split into PROGRAMMING_EXERCISE_CHAT and COURSE_CHAT - TEXT_EXERCISE_CHAT, COMPETENCY_GENERATION, LECTURE_INGESTION + CHAT, // TODO: Rename to PROGRAMMING_EXERCISE_CHAT + TEXT_EXERCISE_CHAT, COURSE_CHAT, COMPETENCY_GENERATION, LECTURE_INGESTION } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedCourseChatSubSettingsDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedCourseChatSubSettingsDTO.java new file mode 100644 index 000000000000..3c1cf365763d --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedCourseChatSubSettingsDTO.java @@ -0,0 +1,13 @@ +package de.tum.cit.aet.artemis.iris.dto; + +import java.util.SortedSet; + +import jakarta.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record IrisCombinedCourseChatSubSettingsDTO(boolean enabled, Integer rateLimit, Integer rateLimitTimeframeHours, @Nullable SortedSet allowedVariants, + @Nullable String selectedVariant) { + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedSettingsDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedSettingsDTO.java index b05645603dbe..294f2e836140 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedSettingsDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedSettingsDTO.java @@ -7,6 +7,7 @@ public record IrisCombinedSettingsDTO( IrisCombinedChatSubSettingsDTO irisChatSettings, IrisCombinedTextExerciseChatSubSettingsDTO irisTextExerciseChatSettings, + IrisCombinedCourseChatSubSettingsDTO irisCourseChatSettings, IrisCombinedLectureIngestionSubSettingsDTO irisLectureIngestionSettings, IrisCombinedCompetencyGenerationSubSettingsDTO irisCompetencyGenerationSettings ) {} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisCourseChatSessionService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisCourseChatSessionService.java index d2743c2e71a5..7e6693991430 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisCourseChatSessionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisCourseChatSessionService.java @@ -90,7 +90,7 @@ public void checkHasAccessTo(User user, IrisCourseChatSession session) { */ @Override public void checkIsFeatureActivatedFor(IrisCourseChatSession session) { - irisSettingsService.isEnabledForElseThrow(IrisSubSettingsType.CHAT, session.getCourse()); + irisSettingsService.isEnabledForElseThrow(IrisSubSettingsType.COURSE_CHAT, session.getCourse()); } @Override @@ -134,7 +134,7 @@ protected void setLLMTokenUsageParameters(LLMTokenUsageService.LLMTokenUsageBuil */ public void onJudgementOfLearningSet(CompetencyJol competencyJol) { var course = competencyJol.getCompetency().getCourse(); - if (!irisSettingsService.isEnabledFor(IrisSubSettingsType.CHAT, course)) { + if (!irisSettingsService.isEnabledFor(IrisSubSettingsType.COURSE_CHAT, course)) { return; } var user = competencyJol.getUser(); @@ -154,7 +154,7 @@ public void onJudgementOfLearningSet(CompetencyJol competencyJol) { */ public IrisCourseChatSession getCurrentSessionOrCreateIfNotExists(Course course, User user, boolean sendInitialMessageIfCreated) { user.hasAcceptedIrisElseThrow(); - irisSettingsService.isEnabledForElseThrow(IrisSubSettingsType.CHAT, course); + irisSettingsService.isEnabledForElseThrow(IrisSubSettingsType.COURSE_CHAT, course); return getCurrentSessionOrCreateIfNotExistsInternal(course, user, sendInitialMessageIfCreated); } @@ -184,7 +184,7 @@ private IrisCourseChatSession getCurrentSessionOrCreateIfNotExistsInternal(Cours */ public IrisCourseChatSession createSession(Course course, User user, boolean sendInitialMessage) { user.hasAcceptedIrisElseThrow(); - irisSettingsService.isEnabledForElseThrow(IrisSubSettingsType.CHAT, course); + irisSettingsService.isEnabledForElseThrow(IrisSubSettingsType.COURSE_CHAT, course); return createSessionInternal(course, user, sendInitialMessage); } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisExerciseChatSessionService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisExerciseChatSessionService.java index a51f1730e98c..20aa684e534a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisExerciseChatSessionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisExerciseChatSessionService.java @@ -2,6 +2,7 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_IRIS; +import java.util.List; import java.util.Objects; import java.util.Optional; @@ -27,6 +28,7 @@ import de.tum.cit.aet.artemis.iris.service.settings.IrisSettingsService; import de.tum.cit.aet.artemis.iris.service.websocket.IrisChatWebsocketService; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; +import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation; import de.tum.cit.aet.artemis.programming.domain.ProgrammingSubmission; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseRepository; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseStudentParticipationRepository; @@ -144,7 +146,14 @@ public void requestAndHandleResponse(IrisExerciseChatSession session) { } private Optional getLatestSubmissionIfExists(ProgrammingExercise exercise, User user) { - var participations = programmingExerciseStudentParticipationRepository.findAllWithSubmissionsByExerciseIdAndStudentLogin(exercise.getId(), user.getLogin()); + List participations; + if (exercise.isTeamMode()) { + participations = programmingExerciseStudentParticipationRepository.findAllWithSubmissionByExerciseIdAndStudentLoginInTeam(exercise.getId(), user.getLogin()); + } + else { + participations = programmingExerciseStudentParticipationRepository.findAllWithSubmissionsByExerciseIdAndStudentLogin(exercise.getId(), user.getLogin()); + } + if (participations.isEmpty()) { return Optional.empty(); } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSettingsService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSettingsService.java index 6047631fb5bf..d286def04e19 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSettingsService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSettingsService.java @@ -32,6 +32,7 @@ import de.tum.cit.aet.artemis.exercise.domain.Exercise; import de.tum.cit.aet.artemis.iris.domain.settings.IrisChatSubSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisCompetencyGenerationSubSettings; +import de.tum.cit.aet.artemis.iris.domain.settings.IrisCourseChatSubSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisCourseSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisExerciseSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisGlobalSettings; @@ -107,6 +108,7 @@ private void createInitialGlobalSettings() { initializeIrisChatSettings(settings); initializeIrisTextExerciseChatSettings(settings); + initializeIrisCourseChatSettings(settings); initializeIrisLectureIngestionSettings(settings); initializeIrisCompetencyGenerationSettings(settings); @@ -135,6 +137,12 @@ private void initializeIrisTextExerciseChatSettings(IrisGlobalSettings settings) settings.setIrisTextExerciseChatSettings(irisChatSettings); } + private void initializeIrisCourseChatSettings(IrisGlobalSettings settings) { + var irisChatSettings = settings.getIrisCourseChatSettings(); + irisChatSettings = initializeSettings(irisChatSettings, IrisCourseChatSubSettings::new); + settings.setIrisCourseChatSettings(irisChatSettings); + } + private void initializeIrisLectureIngestionSettings(IrisGlobalSettings settings) { var irisLectureIngestionSettings = settings.getIrisLectureIngestionSettings(); irisLectureIngestionSettings = initializeSettings(irisLectureIngestionSettings, IrisLectureIngestionSubSettings::new); @@ -207,18 +215,15 @@ private T updateIrisSettings(long existingSettingsId, T var existingSettings = irisSettingsRepository.findByIdElseThrow(existingSettingsId); - if (existingSettings instanceof IrisGlobalSettings globalSettings && settingsUpdate instanceof IrisGlobalSettings globalSettingsUpdate) { - return (T) updateGlobalSettings(globalSettings, globalSettingsUpdate); - } - else if (existingSettings instanceof IrisCourseSettings courseSettings && settingsUpdate instanceof IrisCourseSettings courseSettingsUpdate) { - return (T) updateCourseSettings(courseSettings, courseSettingsUpdate); - } - else if (existingSettings instanceof IrisExerciseSettings exerciseSettings && settingsUpdate instanceof IrisExerciseSettings exerciseSettingsUpdate) { - return (T) updateExerciseSettings(exerciseSettings, exerciseSettingsUpdate); - } - else { - throw new BadRequestAlertException("Unknown Iris settings type", "IrisSettings", "unknownType"); - } + return switch (existingSettings) { + case IrisGlobalSettings globalSettings when settingsUpdate instanceof IrisGlobalSettings globalSettingsUpdate -> + (T) updateGlobalSettings(globalSettings, globalSettingsUpdate); + case IrisCourseSettings courseSettings when settingsUpdate instanceof IrisCourseSettings courseSettingsUpdate -> + (T) updateCourseSettings(courseSettings, courseSettingsUpdate); + case IrisExerciseSettings exerciseSettings when settingsUpdate instanceof IrisExerciseSettings exerciseSettingsUpdate -> + (T) updateExerciseSettings(exerciseSettings, exerciseSettingsUpdate); + case null, default -> throw new BadRequestAlertException("Unknown Iris settings type", "IrisSettings", "unknownType"); + }; } /** @@ -230,29 +235,35 @@ else if (existingSettings instanceof IrisExerciseSettings exerciseSettings && se */ private IrisGlobalSettings updateGlobalSettings(IrisGlobalSettings existingSettings, IrisGlobalSettings settingsUpdate) { // @formatter:off - existingSettings.setIrisLectureIngestionSettings(irisSubSettingsService.update( - existingSettings.getIrisLectureIngestionSettings(), - settingsUpdate.getIrisLectureIngestionSettings(), - null, - GLOBAL + existingSettings.setIrisChatSettings(irisSubSettingsService.update( + existingSettings.getIrisChatSettings(), + settingsUpdate.getIrisChatSettings(), + null, + GLOBAL )); existingSettings.setIrisTextExerciseChatSettings(irisSubSettingsService.update( - existingSettings.getIrisTextExerciseChatSettings(), - settingsUpdate.getIrisTextExerciseChatSettings(), - null, - GLOBAL + existingSettings.getIrisTextExerciseChatSettings(), + settingsUpdate.getIrisTextExerciseChatSettings(), + null, + GLOBAL )); - existingSettings.setIrisChatSettings(irisSubSettingsService.update( - existingSettings.getIrisChatSettings(), - settingsUpdate.getIrisChatSettings(), - null, - GLOBAL + existingSettings.setIrisCourseChatSettings(irisSubSettingsService.update( + existingSettings.getIrisCourseChatSettings(), + settingsUpdate.getIrisCourseChatSettings(), + null, + GLOBAL + )); + existingSettings.setIrisLectureIngestionSettings(irisSubSettingsService.update( + existingSettings.getIrisLectureIngestionSettings(), + settingsUpdate.getIrisLectureIngestionSettings(), + null, + GLOBAL )); existingSettings.setIrisCompetencyGenerationSettings(irisSubSettingsService.update( - existingSettings.getIrisCompetencyGenerationSettings(), - settingsUpdate.getIrisCompetencyGenerationSettings(), - null, - GLOBAL + existingSettings.getIrisCompetencyGenerationSettings(), + settingsUpdate.getIrisCompetencyGenerationSettings(), + null, + GLOBAL )); // @formatter:on @@ -275,28 +286,34 @@ private IrisCourseSettings updateCourseSettings(IrisCourseSettings existingSetti var parentSettings = getCombinedIrisGlobalSettings(); // @formatter:off existingSettings.setIrisChatSettings(irisSubSettingsService.update( - existingSettings.getIrisChatSettings(), - settingsUpdate.getIrisChatSettings(), - parentSettings.irisChatSettings(), - COURSE + existingSettings.getIrisChatSettings(), + settingsUpdate.getIrisChatSettings(), + parentSettings.irisChatSettings(), + COURSE )); existingSettings.setIrisTextExerciseChatSettings(irisSubSettingsService.update( - existingSettings.getIrisTextExerciseChatSettings(), - settingsUpdate.getIrisTextExerciseChatSettings(), - parentSettings.irisTextExerciseChatSettings(), - COURSE + existingSettings.getIrisTextExerciseChatSettings(), + settingsUpdate.getIrisTextExerciseChatSettings(), + parentSettings.irisTextExerciseChatSettings(), + COURSE + )); + existingSettings.setIrisCourseChatSettings(irisSubSettingsService.update( + existingSettings.getIrisCourseChatSettings(), + settingsUpdate.getIrisCourseChatSettings(), + parentSettings.irisCourseChatSettings(), + COURSE )); existingSettings.setIrisLectureIngestionSettings(irisSubSettingsService.update( - existingSettings.getIrisLectureIngestionSettings(), - settingsUpdate.getIrisLectureIngestionSettings(), - parentSettings.irisLectureIngestionSettings(), - COURSE + existingSettings.getIrisLectureIngestionSettings(), + settingsUpdate.getIrisLectureIngestionSettings(), + parentSettings.irisLectureIngestionSettings(), + COURSE )); existingSettings.setIrisCompetencyGenerationSettings(irisSubSettingsService.update( - existingSettings.getIrisCompetencyGenerationSettings(), - settingsUpdate.getIrisCompetencyGenerationSettings(), - parentSettings.irisCompetencyGenerationSettings(), - COURSE + existingSettings.getIrisCompetencyGenerationSettings(), + settingsUpdate.getIrisCompetencyGenerationSettings(), + parentSettings.irisCompetencyGenerationSettings(), + COURSE )); // @formatter:on @@ -430,16 +447,16 @@ private IrisExerciseSettings updateExerciseSettings(IrisExerciseSettings existin var parentSettings = getCombinedIrisSettingsFor(existingSettings.getExercise().getCourseViaExerciseGroupOrCourseMember(), false); // @formatter:off existingSettings.setIrisChatSettings(irisSubSettingsService.update( - existingSettings.getIrisChatSettings(), - settingsUpdate.getIrisChatSettings(), - parentSettings.irisChatSettings(), - EXERCISE + existingSettings.getIrisChatSettings(), + settingsUpdate.getIrisChatSettings(), + parentSettings.irisChatSettings(), + EXERCISE )); existingSettings.setIrisTextExerciseChatSettings(irisSubSettingsService.update( - existingSettings.getIrisTextExerciseChatSettings(), - settingsUpdate.getIrisTextExerciseChatSettings(), - parentSettings.irisTextExerciseChatSettings(), - EXERCISE + existingSettings.getIrisTextExerciseChatSettings(), + settingsUpdate.getIrisTextExerciseChatSettings(), + parentSettings.irisTextExerciseChatSettings(), + EXERCISE )); // @formatter:on return irisSettingsRepository.save(existingSettings); @@ -507,10 +524,11 @@ public IrisCombinedSettingsDTO getCombinedIrisGlobalSettings() { // @formatter:off return new IrisCombinedSettingsDTO( - irisSubSettingsService.combineChatSettings(settingsList, false), - irisSubSettingsService.combineTextExerciseChatSettings(settingsList, false), - irisSubSettingsService.combineLectureIngestionSubSettings(settingsList, false), - irisSubSettingsService.combineCompetencyGenerationSettings(settingsList, false) + irisSubSettingsService.combineChatSettings(settingsList, false), + irisSubSettingsService.combineTextExerciseChatSettings(settingsList, false), + irisSubSettingsService.combineCourseChatSettings(settingsList, false), + irisSubSettingsService.combineLectureIngestionSubSettings(settingsList, false), + irisSubSettingsService.combineCompetencyGenerationSettings(settingsList, false) ); // @formatter:on } @@ -532,10 +550,11 @@ public IrisCombinedSettingsDTO getCombinedIrisSettingsFor(Course course, boolean // @formatter:off return new IrisCombinedSettingsDTO( - irisSubSettingsService.combineChatSettings(settingsList, minimal), - irisSubSettingsService.combineTextExerciseChatSettings(settingsList, minimal), - irisSubSettingsService.combineLectureIngestionSubSettings(settingsList, minimal), - irisSubSettingsService.combineCompetencyGenerationSettings(settingsList, minimal) + irisSubSettingsService.combineChatSettings(settingsList, minimal), + irisSubSettingsService.combineTextExerciseChatSettings(settingsList, minimal), + irisSubSettingsService.combineCourseChatSettings(settingsList, minimal), + irisSubSettingsService.combineLectureIngestionSubSettings(settingsList, minimal), + irisSubSettingsService.combineCompetencyGenerationSettings(settingsList, minimal) ); // @formatter:on } @@ -558,10 +577,11 @@ public IrisCombinedSettingsDTO getCombinedIrisSettingsFor(Exercise exercise, boo // @formatter:off return new IrisCombinedSettingsDTO( - irisSubSettingsService.combineChatSettings(settingsList, minimal), - irisSubSettingsService.combineTextExerciseChatSettings(settingsList, minimal), - irisSubSettingsService.combineLectureIngestionSubSettings(settingsList, minimal), - irisSubSettingsService.combineCompetencyGenerationSettings(settingsList, minimal) + irisSubSettingsService.combineChatSettings(settingsList, minimal), + irisSubSettingsService.combineTextExerciseChatSettings(settingsList, minimal), + irisSubSettingsService.combineCourseChatSettings(settingsList, minimal), + irisSubSettingsService.combineLectureIngestionSubSettings(settingsList, minimal), + irisSubSettingsService.combineCompetencyGenerationSettings(settingsList, minimal) ); // @formatter:on } @@ -587,10 +607,11 @@ public boolean shouldShowMinimalSettings(Exercise exercise, User user) { public IrisCourseSettings getDefaultSettingsFor(Course course) { var settings = new IrisCourseSettings(); settings.setCourse(course); - settings.setIrisLectureIngestionSettings(new IrisLectureIngestionSubSettings()); settings.setIrisChatSettings(new IrisChatSubSettings()); - settings.setIrisCompetencyGenerationSettings(new IrisCompetencyGenerationSubSettings()); settings.setIrisTextExerciseChatSettings(new IrisTextExerciseChatSubSettings()); + settings.setIrisCourseChatSettings(new IrisCourseChatSubSettings()); + settings.setIrisLectureIngestionSettings(new IrisLectureIngestionSubSettings()); + settings.setIrisCompetencyGenerationSettings(new IrisCompetencyGenerationSubSettings()); return settings; } @@ -664,6 +685,7 @@ private boolean isFeatureEnabledInSettings(IrisCombinedSettingsDTO settings, Iri return switch (type) { case CHAT -> settings.irisChatSettings().enabled(); case TEXT_EXERCISE_CHAT -> settings.irisTextExerciseChatSettings().enabled(); + case COURSE_CHAT -> settings.irisCourseChatSettings().enabled(); case COMPETENCY_GENERATION -> settings.irisCompetencyGenerationSettings().enabled(); case LECTURE_INGESTION -> settings.irisLectureIngestionSettings().enabled(); }; diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSubSettingsService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSubSettingsService.java index 2c284b6ea1f8..c6c17601e5af 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSubSettingsService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSubSettingsService.java @@ -17,6 +17,7 @@ import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; import de.tum.cit.aet.artemis.iris.domain.settings.IrisChatSubSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisCompetencyGenerationSubSettings; +import de.tum.cit.aet.artemis.iris.domain.settings.IrisCourseChatSubSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisCourseSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisExerciseSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisLectureIngestionSubSettings; @@ -26,6 +27,7 @@ import de.tum.cit.aet.artemis.iris.domain.settings.IrisTextExerciseChatSubSettings; import de.tum.cit.aet.artemis.iris.dto.IrisCombinedChatSubSettingsDTO; import de.tum.cit.aet.artemis.iris.dto.IrisCombinedCompetencyGenerationSubSettingsDTO; +import de.tum.cit.aet.artemis.iris.dto.IrisCombinedCourseChatSubSettingsDTO; import de.tum.cit.aet.artemis.iris.dto.IrisCombinedLectureIngestionSubSettingsDTO; import de.tum.cit.aet.artemis.iris.dto.IrisCombinedTextExerciseChatSubSettingsDTO; @@ -123,6 +125,37 @@ public IrisTextExerciseChatSubSettings update(IrisTextExerciseChatSubSettings cu return currentSettings; } + /** + * Updates a course chat sub settings object. + * + * @param currentSettings Current chat sub settings. + * @param newSettings Updated chat sub settings. + * @param parentSettings Parent chat sub settings. + * @param settingsType Type of the settings the sub settings belong to. + * @return Updated chat sub settings. + */ + public IrisCourseChatSubSettings update(IrisCourseChatSubSettings currentSettings, IrisCourseChatSubSettings newSettings, IrisCombinedCourseChatSubSettingsDTO parentSettings, + IrisSettingsType settingsType) { + if (newSettings == null) { + if (parentSettings == null) { + throw new IllegalArgumentException("Cannot delete the course chat settings"); + } + return null; + } + if (currentSettings == null) { + currentSettings = new IrisCourseChatSubSettings(); + } + if (authCheckService.isAdmin()) { + currentSettings.setEnabled(newSettings.isEnabled()); + currentSettings.setRateLimit(newSettings.getRateLimit()); + currentSettings.setRateLimitTimeframeHours(newSettings.getRateLimitTimeframeHours()); + } + currentSettings.setAllowedVariants(selectAllowedVariants(currentSettings.getAllowedVariants(), newSettings.getAllowedVariants())); + currentSettings.setSelectedVariant(validateSelectedVariant(currentSettings.getSelectedVariant(), newSettings.getSelectedVariant(), currentSettings.getAllowedVariants(), + parentSettings != null ? parentSettings.allowedVariants() : null)); + return currentSettings; + } + /** * Updates a Lecture Ingestion sub settings object. * If the new settings are null, the current settings will be deleted (except if the parent settings are null == if the settings are global). @@ -224,6 +257,24 @@ private String validateSelectedVariant(String selectedVariant, String newSelecte return selectedVariant; } + /** + * Combines the chat settings of multiple {@link IrisSettings} objects. + * If minimal is true, the returned object will only contain the enabled and rateLimit fields. + * The minimal version can safely be sent to students. + * + * @param settingsList List of {@link IrisSettings} objects to combine. + * @param minimal Whether to return a minimal version of the combined settings. + * @return Combined chat settings. + */ + public IrisCombinedChatSubSettingsDTO combineChatSettings(ArrayList settingsList, boolean minimal) { + var enabled = getCombinedEnabled(settingsList, IrisSettings::getIrisChatSettings); + var rateLimit = getCombinedRateLimit(settingsList); + var allowedVariants = !minimal ? getCombinedAllowedVariants(settingsList, IrisSettings::getIrisChatSettings) : null; + var selectedVariant = !minimal ? getCombinedSelectedVariant(settingsList, IrisSettings::getIrisChatSettings) : null; + var enabledForCategories = !minimal ? getCombinedEnabledForCategories(settingsList, IrisSettings::getIrisChatSettings) : null; + return new IrisCombinedChatSubSettingsDTO(enabled, rateLimit, null, allowedVariants, selectedVariant, enabledForCategories); + } + /** * Combines the chat settings of multiple {@link IrisSettings} objects. * If minimal is true, the returned object will only contain the enabled and rateLimit fields. @@ -251,13 +302,12 @@ public IrisCombinedTextExerciseChatSubSettingsDTO combineTextExerciseChatSetting * @param minimal Whether to return a minimal version of the combined settings. * @return Combined chat settings. */ - public IrisCombinedChatSubSettingsDTO combineChatSettings(ArrayList settingsList, boolean minimal) { - var enabled = getCombinedEnabled(settingsList, IrisSettings::getIrisChatSettings); + public IrisCombinedCourseChatSubSettingsDTO combineCourseChatSettings(ArrayList settingsList, boolean minimal) { + var enabled = getCombinedEnabled(settingsList, IrisSettings::getIrisCourseChatSettings); var rateLimit = getCombinedRateLimit(settingsList); var allowedVariants = !minimal ? getCombinedAllowedVariants(settingsList, IrisSettings::getIrisChatSettings) : null; var selectedVariant = !minimal ? getCombinedSelectedVariant(settingsList, IrisSettings::getIrisChatSettings) : null; - var enabledForCategories = !minimal ? getCombinedEnabledForCategories(settingsList, IrisSettings::getIrisChatSettings) : null; - return new IrisCombinedChatSubSettingsDTO(enabled, rateLimit, null, allowedVariants, selectedVariant, enabledForCategories); + return new IrisCombinedCourseChatSubSettingsDTO(enabled, rateLimit, null, allowedVariants, selectedVariant); } /** @@ -350,6 +400,14 @@ private String getCombinedSelectedVariant(List settingsList, Funct .filter(model -> model != null && !model.isBlank()).reduce((first, second) -> second).orElse(null); } + /** + * Combines the enabledForCategories field of multiple {@link IrisSettings} objects. + * Simply &&s all enabledForCategories fields together. + * + * @param settingsList List of {@link IrisSettings} objects to combine. + * @param subSettingsFunction Function to get the sub settings from an IrisSettings object. + * @return Combined enabledForCategories field. + */ private SortedSet getCombinedEnabledForCategories(List settingsList, Function subSettingsFunction) { return settingsList.stream().filter(Objects::nonNull).filter(settings -> settings instanceof IrisCourseSettings).map(subSettingsFunction).filter(Objects::nonNull) .map(IrisChatSubSettings::getEnabledForCategories).filter(Objects::nonNull).filter(models -> !models.isEmpty()).reduce((first, second) -> second) diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisCourseChatSessionResource.java b/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisCourseChatSessionResource.java index 13c7a1b5894d..583776c922c7 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisCourseChatSessionResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisCourseChatSessionResource.java @@ -91,7 +91,7 @@ public ResponseEntity getCurrentSessionOrCreateIfNotExist public ResponseEntity> getAllSessions(@PathVariable Long courseId) { var course = courseRepository.findByIdElseThrow(courseId); - irisSettingsService.isEnabledForElseThrow(IrisSubSettingsType.CHAT, course); + irisSettingsService.isEnabledForElseThrow(IrisSubSettingsType.COURSE_CHAT, course); var user = userRepository.getUserWithGroupsAndAuthorities(); user.hasAcceptedIrisElseThrow(); diff --git a/src/main/java/de/tum/cit/aet/artemis/modeling/service/ModelingExerciseFeedbackService.java b/src/main/java/de/tum/cit/aet/artemis/modeling/service/ModelingExerciseFeedbackService.java index e6e229e79f6a..fc026f73988c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/modeling/service/ModelingExerciseFeedbackService.java +++ b/src/main/java/de/tum/cit/aet/artemis/modeling/service/ModelingExerciseFeedbackService.java @@ -59,15 +59,6 @@ public ModelingExerciseFeedbackService(Optional athenaResults = participation.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA).toList(); - - if (athenaResults.size() >= 10) { - throw new BadRequestAlertException("Maximum number of AI feedback requests reached.", "participation", "preconditions not met"); - } - } - /** * Handles the request for generating feedback for a modeling exercise. * Unlike programming exercises a tutor is not notified if Athena is not available. @@ -79,6 +70,7 @@ private void checkRateLimitOrThrow(StudentParticipation participation) { public StudentParticipation handleNonGradedFeedbackRequest(StudentParticipation participation, ModelingExercise modelingExercise) { if (this.athenaFeedbackSuggestionsService.isPresent()) { this.checkRateLimitOrThrow(participation); + this.checkLatestSubmissionHasAthenaResultOrThrow(participation); CompletableFuture.runAsync(() -> this.generateAutomaticNonGradedFeedback(participation, modelingExercise)); } return participation; @@ -125,6 +117,10 @@ public void generateAutomaticNonGradedFeedback(StudentParticipation participatio } catch (Exception e) { log.error("Could not generate feedback for exercise ID: {} and participation ID: {}", modelingExercise.getId(), participation.getId(), e); + automaticResult.setSuccessful(false); + automaticResult.setCompletionDate(null); + participation.addResult(automaticResult); + this.resultWebsocketService.broadcastNewResult(participation, automaticResult); throw new InternalServerErrorException("Something went wrong... AI Feedback could not be generated"); } } @@ -173,6 +169,7 @@ private Feedback convertToFeedback(ModelingFeedbackDTO feedbackItem) { feedback.setHasLongFeedbackText(false); feedback.setType(FeedbackType.AUTOMATIC); feedback.setCredits(feedbackItem.credits()); + feedback.setReference(feedbackItem.reference()); return feedback; } @@ -193,4 +190,45 @@ private double calculateTotalFeedbackScore(List feedbacks, ModelingExe return (totalCredits / maxPoints) * 100; } + + /** + * Checks if the number of Athena results for the given participation exceeds + * the allowed threshold and throws an exception if the limit is reached. + * + * @param participation the student participation to check + * @throws BadRequestAlertException if the maximum number of Athena feedback requests is exceeded + */ + private void checkRateLimitOrThrow(StudentParticipation participation) { + List athenaResults = participation.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA).toList(); + + if (athenaResults.size() >= 10) { + throw new BadRequestAlertException("Maximum number of AI feedback requests reached.", "participation", "maxAthenaResultsReached", true); + } + } + + /** + * Ensures that the latest submission associated with the participation does not already + * have an Athena-generated result. Throws an exception if Athena result already exists. + * + * @param participation the student participation to validate + * @throws BadRequestAlertException if no legal submissions exist or if an Athena result is already present + */ + private void checkLatestSubmissionHasAthenaResultOrThrow(StudentParticipation participation) { + Optional submissionOptional = participationService.findExerciseParticipationWithLatestSubmissionAndResultElseThrow(participation.getId()) + .findLatestSubmission(); + + if (submissionOptional.isEmpty()) { + throw new BadRequestAlertException("No legal submissions found", "submission", "noSubmission"); + } + + Submission submission = submissionOptional.get(); + + Result latestResult = submission.getLatestResult(); + + if (latestResult != null && latestResult.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA) { + log.debug("Submission ID: {} already has an Athena result. Skipping feedback generation.", submission.getId()); + throw new BadRequestAlertException("Submission already has an Athena result", "submission", "submissionAlreadyHasAthenaResult", true); + } + } + } diff --git a/src/main/java/de/tum/cit/aet/artemis/modeling/web/ModelingSubmissionResource.java b/src/main/java/de/tum/cit/aet/artemis/modeling/web/ModelingSubmissionResource.java index 501309aea8e8..2ca79ac3897e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/modeling/web/ModelingSubmissionResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/modeling/web/ModelingSubmissionResource.java @@ -3,6 +3,7 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import java.util.ArrayList; +import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Optional; @@ -31,6 +32,8 @@ import de.tum.cit.aet.artemis.assessment.domain.GradingCriterion; import de.tum.cit.aet.artemis.assessment.domain.Result; import de.tum.cit.aet.artemis.assessment.repository.GradingCriterionRepository; +import de.tum.cit.aet.artemis.assessment.repository.ResultRepository; +import de.tum.cit.aet.artemis.assessment.service.ResultService; import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException; import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; @@ -67,6 +70,8 @@ public class ModelingSubmissionResource extends AbstractSubmissionResource { private static final String ENTITY_NAME = "modelingSubmission"; + private final ResultRepository resultRepository; + @Value("${jhipster.clientApp.name}") private String applicationName; @@ -82,10 +87,12 @@ public class ModelingSubmissionResource extends AbstractSubmissionResource { private final PlagiarismService plagiarismService; + private final ResultService resultService; + public ModelingSubmissionResource(SubmissionRepository submissionRepository, ModelingSubmissionService modelingSubmissionService, ModelingExerciseRepository modelingExerciseRepository, AuthorizationCheckService authCheckService, UserRepository userRepository, ExerciseRepository exerciseRepository, GradingCriterionRepository gradingCriterionRepository, ExamSubmissionService examSubmissionService, StudentParticipationRepository studentParticipationRepository, - ModelingSubmissionRepository modelingSubmissionRepository, PlagiarismService plagiarismService) { + ModelingSubmissionRepository modelingSubmissionRepository, PlagiarismService plagiarismService, ResultService resultService, ResultRepository resultRepository) { super(submissionRepository, authCheckService, userRepository, exerciseRepository, modelingSubmissionService, studentParticipationRepository); this.modelingSubmissionService = modelingSubmissionService; this.modelingExerciseRepository = modelingExerciseRepository; @@ -93,6 +100,8 @@ public ModelingSubmissionResource(SubmissionRepository submissionRepository, Mod this.examSubmissionService = examSubmissionService; this.modelingSubmissionRepository = modelingSubmissionRepository; this.plagiarismService = plagiarismService; + this.resultService = resultService; + this.resultRepository = resultRepository; } /** @@ -367,4 +376,81 @@ public ResponseEntity getLatestSubmissionForModelingEditor(@ return ResponseEntity.ok(modelingSubmission); } + + /** + * GET /participations/{participationId}/submissions-with-results : get submissions with results for a particular student participation. + * When the assessment period is not over yet, only submissions with Athena results are returned. + * When the assessment period is over, both Athena and normal results are returned. + * + * @param participationId the id of the participation for which to get the submissions with results + * @return the ResponseEntity with status 200 (OK) and with body the list of submissions with results and feedbacks, or with status 404 (Not Found) if the participation could + * not be found + */ + @GetMapping("participations/{participationId}/submissions-with-results") + @EnforceAtLeastStudent + public ResponseEntity> getSubmissionsWithResultsForParticipation(@PathVariable long participationId) { + log.debug("REST request to get submissions with results for participation: {}", participationId); + + // Retrieve and check the participation + StudentParticipation participation = studentParticipationRepository.findByIdWithLegalSubmissionsResultsFeedbackElseThrow(participationId); + User user = userRepository.getUserWithGroupsAndAuthorities(); + + if (participation.getExercise() == null) { + return ResponseEntity.badRequest() + .headers(HeaderUtil.createFailureAlert(applicationName, true, "modelingExercise", "exerciseEmpty", "The exercise belonging to the participation is null.")) + .body(null); + } + + if (!(participation.getExercise() instanceof ModelingExercise modelingExercise)) { + return ResponseEntity.badRequest().headers( + HeaderUtil.createFailureAlert(applicationName, true, "modelingExercise", "wrongExerciseType", "The exercise of the participation is not a modeling exercise.")) + .body(null); + } + + // Students can only see their own models (to prevent cheating). TAs, instructors and admins can see all models. + boolean isAtLeastTutor = authCheckService.isAtLeastTeachingAssistantForExercise(modelingExercise, user); + if (!(authCheckService.isOwnerOfParticipation(participation) || isAtLeastTutor)) { + throw new AccessForbiddenException(); + } + + // Exam exercises cannot be seen by students between the endDate and the publishResultDate + if (!authCheckService.isAllowedToGetExamResult(modelingExercise, participation, user)) { + throw new AccessForbiddenException(); + } + + boolean isStudent = !isAtLeastTutor; + + // Get the submissions associated with the participation + Set submissions = participation.getSubmissions(); + + // Filter submissions to only include those with relevant results + List submissionsWithResults = submissions.stream().filter(submission -> { + + submission.setParticipation(participation); + + // Filter results within each submission based on assessment type and period + List filteredResults = submission.getResults().stream().filter(result -> { + if (isStudent) { + if (ExerciseDateService.isAfterAssessmentDueDate(modelingExercise)) { + return true; // Include all results if the assessment period is over + } + else { + return result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA; // Only include Athena results if the assessment period is not over + } + } + else { + return true; // Tutors and above can see all results + } + }).peek(Result::filterSensitiveInformation).sorted(Comparator.comparing(Result::getCompletionDate).reversed()).toList(); + + // Set filtered results back into the submission if any results remain after filtering + if (!filteredResults.isEmpty()) { + submission.setResults(filteredResults); + return true; // Include submission as it has relevant results + } + return false; + }).toList(); + + return ResponseEntity.ok().body(submissionsWithResults); + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExerciseBuildConfig.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExerciseBuildConfig.java index a5ada6708999..d28e21bb3ad1 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExerciseBuildConfig.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExerciseBuildConfig.java @@ -60,7 +60,7 @@ public class ProgrammingExerciseBuildConfig extends DomainObject { @Column(name = "timeout_seconds") private int timeoutSeconds; - @Column(name = "docker_flags") + @Column(name = "docker_flags", columnDefinition = "longtext") private String dockerFlags; @OneToOne(mappedBy = "buildConfig") diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseStudentParticipationRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseStudentParticipationRepository.java index c88024f0835b..a126934267ae 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseStudentParticipationRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseStudentParticipationRepository.java @@ -195,6 +195,18 @@ Page findRepositoryUrisByRecentDueDateOrRecentExamEndDate(@Param("earlie """) List findAllWithSubmissionsByExerciseIdAndStudentLogin(@Param("exerciseId") long exerciseId, @Param("username") String username); + @Query(""" + SELECT participation + FROM ProgrammingExerciseStudentParticipation participation + LEFT JOIN FETCH participation.team team + LEFT JOIN FETCH team.students student + LEFT JOIN FETCH participation.submissions + WHERE participation.exercise.id = :exerciseId + AND student.login = :username + ORDER BY participation.testRun ASC + """) + List findAllWithSubmissionByExerciseIdAndStudentLoginInTeam(@Param("exerciseId") long exerciseId, @Param("username") String username); + @EntityGraph(type = LOAD, attributePaths = "team.students") Optional findWithTeamStudentsById(long participationId); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/GitService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/GitService.java index dab2a60def8c..438b9d06b3bd 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/GitService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/GitService.java @@ -392,7 +392,7 @@ public Repository getOrCheckoutRepository(VcsRepositoryUri sourceRepoUri, VcsRep // Clone repository. try { var gitUriAsString = getGitUriAsString(sourceRepoUri); - log.info("Cloning from {} to {}", gitUriAsString, localPath); + log.debug("Cloning from {} to {}", gitUriAsString, localPath); cloneInProgressOperations.put(localPath, localPath); // make sure the directory to copy into is empty FileUtils.deleteDirectory(localPath.toFile()); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseBuildConfigService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseBuildConfigService.java new file mode 100644 index 000000000000..5ccf7f2045a6 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseBuildConfigService.java @@ -0,0 +1,90 @@ +package de.tum.cit.aet.artemis.programming.service; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import jakarta.annotation.Nullable; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import de.tum.cit.aet.artemis.buildagent.dto.DockerFlagsDTO; +import de.tum.cit.aet.artemis.buildagent.dto.DockerRunConfig; +import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseBuildConfig; + +@Profile(PROFILE_CORE) +@Service +public class ProgrammingExerciseBuildConfigService { + + private static final Logger log = org.slf4j.LoggerFactory.getLogger(ProgrammingExerciseBuildConfigService.class); + + private final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Converts a JSON string representing Docker flags (in JSON format) + * into a {@link DockerRunConfig} instance. + * + *

+ * The JSON string is expected to represent a {@link DockerFlagsDTO} object. + * Example JSON input: + * + *

+     * {"network":"none","env":{"key1":"value1","key2":"value2"}}
+     * 
+ * + * @param buildConfig the build config containing the Docker flags + * @return a {@link DockerRunConfig} object initialized with the parsed flags, or {@code null} if the JSON string is empty + */ + @Nullable + public DockerRunConfig getDockerRunConfig(ProgrammingExerciseBuildConfig buildConfig) { + DockerFlagsDTO dockerFlagsDTO = parseDockerFlags(buildConfig); + + return getDockerRunConfigFromParsedFlags(dockerFlagsDTO); + } + + DockerRunConfig getDockerRunConfigFromParsedFlags(DockerFlagsDTO dockerFlagsDTO) { + if (dockerFlagsDTO == null) { + return null; + } + List env = new ArrayList<>(); + boolean isNetworkDisabled = dockerFlagsDTO.network() != null && dockerFlagsDTO.network().equals("none"); + + if (dockerFlagsDTO.env() != null) { + for (Map.Entry entry : dockerFlagsDTO.env().entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + env.add(key + "=" + value); + } + } + + return new DockerRunConfig(isNetworkDisabled, env); + } + + /** + * Parses the JSON string representing Docker flags into DockerFlagsDTO. (see {@link DockerFlagsDTO}) + * + * @return a list of key-value pairs, or {@code null} if the JSON string is empty + * @throws IllegalArgumentException if the JSON string is invalid + */ + @Nullable + DockerFlagsDTO parseDockerFlags(ProgrammingExerciseBuildConfig buildConfig) { + if (StringUtils.isBlank(buildConfig.getDockerFlags())) { + return null; + } + + try { + return objectMapper.readValue(buildConfig.getDockerFlags(), DockerFlagsDTO.class); + } + catch (Exception e) { + log.error("Failed to parse DockerRunConfig from JSON string: {}. Using default settings.", buildConfig.getDockerFlags()); + throw new IllegalArgumentException("Failed to parse DockerRunConfig from JSON string: " + buildConfig.getDockerFlags(), e); + } + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseService.java index 20b546ad4a48..c60fbc5b9d34 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseService.java @@ -1,6 +1,7 @@ package de.tum.cit.aet.artemis.programming.service; import static de.tum.cit.aet.artemis.core.config.Constants.ALLOWED_CHECKOUT_DIRECTORY; +import static de.tum.cit.aet.artemis.core.config.Constants.MAX_ENVIRONMENT_VARIABLES_DOCKER_FLAG_LENGTH; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import static de.tum.cit.aet.artemis.programming.domain.build.BuildPlanType.SOLUTION; import static de.tum.cit.aet.artemis.programming.domain.build.BuildPlanType.TEMPLATE; @@ -46,6 +47,8 @@ import de.tum.cit.aet.artemis.assessment.repository.ResultRepository; import de.tum.cit.aet.artemis.atlas.api.CompetencyProgressApi; +import de.tum.cit.aet.artemis.buildagent.dto.DockerFlagsDTO; +import de.tum.cit.aet.artemis.buildagent.dto.DockerRunConfig; import de.tum.cit.aet.artemis.communication.service.conversation.ChannelService; import de.tum.cit.aet.artemis.communication.service.notifications.GroupNotificationScheduleService; import de.tum.cit.aet.artemis.core.domain.Course; @@ -185,6 +188,8 @@ public class ProgrammingExerciseService { private final CompetencyProgressApi competencyProgressApi; + private final ProgrammingExerciseBuildConfigService programmingExerciseBuildConfigService; + public ProgrammingExerciseService(ProgrammingExerciseRepository programmingExerciseRepository, GitService gitService, Optional versionControlService, Optional continuousIntegrationService, Optional continuousIntegrationTriggerService, TemplateProgrammingExerciseParticipationRepository templateProgrammingExerciseParticipationRepository, @@ -199,7 +204,8 @@ public ProgrammingExerciseService(ProgrammingExerciseRepository programmingExerc ProgrammingSubmissionService programmingSubmissionService, Optional irisSettingsService, Optional aeolusTemplateService, Optional buildScriptGenerationService, ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, ProfileService profileService, ExerciseService exerciseService, - ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository, CompetencyProgressApi competencyProgressApi) { + ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository, CompetencyProgressApi competencyProgressApi, + ProgrammingExerciseBuildConfigService programmingExerciseBuildConfigService) { this.programmingExerciseRepository = programmingExerciseRepository; this.gitService = gitService; this.versionControlService = versionControlService; @@ -233,6 +239,7 @@ public ProgrammingExerciseService(ProgrammingExerciseRepository programmingExerc this.exerciseService = exerciseService; this.programmingExerciseBuildConfigRepository = programmingExerciseBuildConfigRepository; this.competencyProgressApi = competencyProgressApi; + this.programmingExerciseBuildConfigService = programmingExerciseBuildConfigService; } /** @@ -372,6 +379,7 @@ public void validateNewProgrammingExerciseSettings(ProgrammingExercise programmi programmingExercise.validateProgrammingSettings(); programmingExercise.validateSettingsForFeedbackRequest(); validateCustomCheckoutPaths(programmingExercise); + validateDockerFlags(programmingExercise); auxiliaryRepositoryService.validateAndAddAuxiliaryRepositoriesOfProgrammingExercise(programmingExercise, programmingExercise.getAuxiliaryRepositories()); submissionPolicyService.validateSubmissionPolicyCreation(programmingExercise); @@ -1072,4 +1080,40 @@ public ProgrammingExercise loadProgrammingExerciseWithAuxiliaryRepositories(long final Set fetchOptions = Set.of(AuxiliaryRepositories); return programmingExerciseRepository.findByIdWithDynamicFetchElseThrow(exerciseId, fetchOptions); } + + /** + * Validates the network access feature for the given programming language. + * Currently, SWIFT and HASKELL do not support disabling the network access feature. + * + * @param programmingExercise the programming exercise to validate + */ + public void validateDockerFlags(ProgrammingExercise programmingExercise) { + ProgrammingExerciseBuildConfig buildConfig = programmingExercise.getBuildConfig(); + DockerFlagsDTO dockerFlagsDTO; + try { + dockerFlagsDTO = programmingExerciseBuildConfigService.parseDockerFlags(buildConfig); + } + catch (IllegalArgumentException e) { + throw new BadRequestAlertException("Error while parsing the docker flags", "Exercise", "dockerFlagsParsingError"); + } + + if (dockerFlagsDTO == null) { + return; + } + + if (dockerFlagsDTO.env() != null) { + for (var entry : dockerFlagsDTO.env().entrySet()) { + if (entry.getKey().length() > MAX_ENVIRONMENT_VARIABLES_DOCKER_FLAG_LENGTH || entry.getValue().length() > MAX_ENVIRONMENT_VARIABLES_DOCKER_FLAG_LENGTH) { + throw new BadRequestAlertException("The environment variables are too long. Max " + MAX_ENVIRONMENT_VARIABLES_DOCKER_FLAG_LENGTH + " chars", "Exercise", + "envVariablesTooLong"); + } + } + } + + DockerRunConfig dockerRunConfig = programmingExerciseBuildConfigService.getDockerRunConfigFromParsedFlags(dockerFlagsDTO); + + if (List.of(ProgrammingLanguage.SWIFT, ProgrammingLanguage.HASKELL).contains(programmingExercise.getProgrammingLanguage()) && dockerRunConfig.isNetworkDisabled()) { + throw new BadRequestAlertException("This programming language does not support disabling the network access feature", "Exercise", "networkAccessNotSupported"); + } + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIQueueWebsocketService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIQueueWebsocketService.java index d25304141c24..0e081a93728b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIQueueWebsocketService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIQueueWebsocketService.java @@ -174,7 +174,7 @@ private static List removeUnnecessaryInformation(List versionControlService, SolutionProgrammingExerciseParticipationRepository solutionProgrammingExerciseParticipationRepository, LocalCIBuildConfigurationService localCIBuildConfigurationService, GitService gitService, ExerciseDateService exerciseDateService, - ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository, BuildScriptProviderService buildScriptProviderService) { + ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository, BuildScriptProviderService buildScriptProviderService, + ProgrammingExerciseBuildConfigService programmingExerciseBuildConfigService) { this.hazelcastInstance = hazelcastInstance; this.aeolusTemplateService = aeolusTemplateService; this.programmingLanguageConfiguration = programmingLanguageConfiguration; @@ -119,6 +124,7 @@ public LocalCITriggerService(@Qualifier("hazelcastInstance") HazelcastInstance h this.programmingExerciseBuildConfigRepository = programmingExerciseBuildConfigRepository; this.exerciseDateService = exerciseDateService; this.buildScriptProviderService = buildScriptProviderService; + this.programmingExerciseBuildConfigService = programmingExerciseBuildConfigService; } @PostConstruct @@ -203,7 +209,8 @@ else if (triggeredByPushTo.equals(RepositoryType.TESTS)) { programmingExercise.getId(), 0, priority, null, repositoryInfo, jobTimingInfo, buildConfig, null); queue.add(buildJobQueueItem); - log.info("Added build job {} to the queue", buildJobId); + log.info("Added build job {} for exercise {} and participation {} with priority {} to the queue", buildJobId, programmingExercise.getShortName(), participation.getId(), + priority); dockerImageCleanupInfo.put(buildConfig.dockerImage(), jobTimingInfo.submissionDate()); } @@ -310,6 +317,8 @@ private BuildConfig getBuildConfig(ProgrammingExerciseParticipation participatio dockerImage = programmingLanguageConfiguration.getImage(programmingExercise.getProgrammingLanguage(), Optional.ofNullable(programmingExercise.getProjectType())); } + DockerRunConfig dockerRunConfig = programmingExerciseBuildConfigService.getDockerRunConfig(buildConfig); + List resultPaths = getTestResultPaths(windfile); resultPaths = buildScriptProviderService.replaceResultPathsPlaceholders(resultPaths, buildConfig); @@ -319,7 +328,7 @@ private BuildConfig getBuildConfig(ProgrammingExerciseParticipation participatio return new BuildConfig(buildScript, dockerImage, commitHashToBuild, assignmentCommitHash, testCommitHash, branch, programmingLanguage, projectType, staticCodeAnalysisEnabled, sequentialTestRunsEnabled, testwiseCoverageEnabled, resultPaths, buildConfig.getTimeoutSeconds(), - buildConfig.getAssignmentCheckoutPath(), buildConfig.getTestCheckoutPath(), buildConfig.getSolutionCheckoutPath()); + buildConfig.getAssignmentCheckoutPath(), buildConfig.getTestCheckoutPath(), buildConfig.getSolutionCheckoutPath(), dockerRunConfig); } private ProgrammingExerciseBuildConfig loadBuildConfig(ProgrammingExercise programmingExercise) { diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/GitPublickeyAuthenticatorService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/GitPublickeyAuthenticatorService.java index 3c9bc85f83a4..165154b565e5 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/GitPublickeyAuthenticatorService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/GitPublickeyAuthenticatorService.java @@ -112,12 +112,17 @@ private boolean authenticateUser(UserSshPublicKey storedKey, PublicKey providedK * @return true if the authentication succeeds, and false if it doesn't */ private boolean authenticateBuildAgent(PublicKey providedKey, ServerSession session) { - if (localCIBuildJobQueueService.isPresent() - && localCIBuildJobQueueService.get().getBuildAgentInformation().stream().anyMatch(agent -> checkPublicKeyMatchesBuildAgentPublicKey(agent, providedKey))) { - - log.info("Authenticating as build agent"); - session.setAttribute(SshConstants.IS_BUILD_AGENT_KEY, true); - return true; + if (localCIBuildJobQueueService.isPresent()) { + // Find the build agent that matches the provided key + Optional matchingAgent = localCIBuildJobQueueService.get().getBuildAgentInformation().stream() + .filter(agent -> checkPublicKeyMatchesBuildAgentPublicKey(agent, providedKey)).findFirst(); + + if (matchingAgent.isPresent()) { + var agent = matchingAgent.get().buildAgent(); + log.debug("Authenticating build agent {} on address {}", agent.displayName(), agent.memberAddress()); + session.setAttribute(SshConstants.IS_BUILD_AGENT_KEY, true); + return true; + } } return false; } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseExportImportResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseExportImportResource.java index 748645568dc6..da74833808c0 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseExportImportResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseExportImportResource.java @@ -78,6 +78,7 @@ import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseExportService; import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseImportFromFileService; import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseImportService; +import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseService; import de.tum.cit.aet.artemis.programming.service.ProgrammingLanguageFeature; import de.tum.cit.aet.artemis.programming.service.ProgrammingLanguageFeatureService; import de.tum.cit.aet.artemis.programming.service.SubmissionPolicyService; @@ -130,13 +131,15 @@ public class ProgrammingExerciseExportImportResource { private final Optional athenaModuleService; + private final ProgrammingExerciseService programmingExerciseService; + public ProgrammingExerciseExportImportResource(ProgrammingExerciseRepository programmingExerciseRepository, UserRepository userRepository, AuthorizationCheckService authCheckService, CourseService courseService, ProgrammingExerciseImportService programmingExerciseImportService, ProgrammingExerciseExportService programmingExerciseExportService, Optional programmingLanguageFeatureService, AuxiliaryRepositoryRepository auxiliaryRepositoryRepository, SubmissionPolicyService submissionPolicyService, ProgrammingExerciseTaskRepository programmingExerciseTaskRepository, ExamAccessService examAccessService, CourseRepository courseRepository, ProgrammingExerciseImportFromFileService programmingExerciseImportFromFileService, ConsistencyCheckService consistencyCheckService, - Optional athenaModuleService, CompetencyProgressApi competencyProgressApi) { + Optional athenaModuleService, CompetencyProgressApi competencyProgressApi, ProgrammingExerciseService programmingExerciseService) { this.programmingExerciseRepository = programmingExerciseRepository; this.userRepository = userRepository; this.courseService = courseService; @@ -153,6 +156,7 @@ public ProgrammingExerciseExportImportResource(ProgrammingExerciseRepository pro this.consistencyCheckService = consistencyCheckService; this.athenaModuleService = athenaModuleService; this.competencyProgressApi = competencyProgressApi; + this.programmingExerciseService = programmingExerciseService; } /** @@ -199,6 +203,7 @@ public ResponseEntity importProgrammingExercise(@PathVariab newExercise.validateGeneralSettings(); newExercise.validateProgrammingSettings(); newExercise.validateSettingsForFeedbackRequest(); + programmingExerciseService.validateDockerFlags(newExercise); validateStaticCodeAnalysisSettings(newExercise); final User user = userRepository.getUserWithGroupsAndAuthorities(); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java index 0a39cabc4e06..0eaab82ce448 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java @@ -329,6 +329,9 @@ public ResponseEntity updateProgrammingExercise(@RequestBod // Verify that the checkout directories have not been changed. This is required since the buildScript and result paths are determined during the creation of the exercise. programmingExerciseService.validateCheckoutDirectoriesUnchanged(programmingExerciseBeforeUpdate, updatedProgrammingExercise); + // Verify that the programming language supports the selected network access option + programmingExerciseService.validateDockerFlags(updatedProgrammingExercise); + // Verify that a theia image is provided when the online IDE is enabled if (updatedProgrammingExercise.isAllowOnlineIde() && updatedProgrammingExercise.getBuildConfig().getTheiaImage() == null) { throw new BadRequestAlertException("You need to provide a Theia image when the online IDE is enabled", ENTITY_NAME, "noTheiaImageProvided"); diff --git a/src/main/resources/config/application-dev.yml b/src/main/resources/config/application-dev.yml index 51563c3c3898..93543e7bbbfe 100644 --- a/src/main/resources/config/application-dev.yml +++ b/src/main/resources/config/application-dev.yml @@ -141,8 +141,9 @@ theia: c: C: "ghcr.io/ls1intum/theia/c:latest" -# Telemetry service: disabled for development artemis: + push-notification-relay: https://hermes-sandbox.artemis.cit.tum.de + # Telemetry service: disabled for development telemetry: enabled: false # Disable sending any telemetry information to the telemetry service by setting this to false sendAdminDetails: false # Include the admins email and name in the telemetry data. Set to false to disable diff --git a/src/main/resources/config/liquibase/changelog/20241022120000_changelog.xml b/src/main/resources/config/liquibase/changelog/20241022120000_changelog.xml new file mode 100644 index 000000000000..193a6370c0ed --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241022120000_changelog.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241114122713_changelog.xml b/src/main/resources/config/liquibase/changelog/20241114122713_changelog.xml new file mode 100644 index 000000000000..a723075a6cde --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241114122713_changelog.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241119191919_changelog.xml b/src/main/resources/config/liquibase/changelog/20241119191919_changelog.xml new file mode 100644 index 000000000000..4ad2d701458c --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241119191919_changelog.xml @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241125000900_changelog.xml b/src/main/resources/config/liquibase/changelog/20241125000900_changelog.xml new file mode 100644 index 000000000000..dc9ee6d41854 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241125000900_changelog.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index d331337ceef4..a2f522a1674c 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -32,10 +32,14 @@ + + + + diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index f171deb555b5..4fbd7b420e03 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -12,6 +12,9 @@ + + + @@ -40,109 +79,111 @@ } -
- @if (submission && (isActive || isLate) && !result && (!isLate || !submission.submitted)) { -
- - - @if (modelingExercise.teamMode) { - - } -
- } - @if ((!isActive || result) && (!isLate || submission.submitted)) { -
-
- +
+ @if (submission && (isActive || isLate) && !result && (!isLate || !submission.submitted) && !isFeedbackView) { +
+ + + @if (modelingExercise.teamMode) { + + }
-
- } - @if (submission?.submitted && (!isActive || result)) { -
-

- @if (!assessmentResult || !assessmentResult!.feedbacks || assessmentResult!.feedbacks!.length === 0) { -

- } - @if (assessmentResult && assessmentResult!.feedbacks && assessmentResult!.feedbacks!.length > 0) { -

- - - - - - - - @if (assessmentsNames) { - - @for (feedback of referencedFeedback; track feedback) { - - - - - } - - } -
- @if (feedback.reference) { - {{ assessmentsNames[feedback.referenceId!]?.type }} - } - @if (feedback.reference) { - {{ assessmentsNames[feedback.referenceId!]?.name }} - } - @if (feedback.reference) { -
- } - @if (feedback.text || feedback.detailText || feedback.gradingInstruction) { - Feedback: - } -
- {{ feedback.credits | number: '1.0-1' }} - @if (feedback.isSubsequent) { - - } -
- } -
- } + } + @if (((!isActive || result) && (!isLate || submission.submitted)) || isFeedbackView) { +
+
+ +
+
+ } + @if ((submission?.submitted && (!isActive || result)) || isFeedbackView) { +
+

+ @if (!assessmentResult || !assessmentResult!.feedbacks || assessmentResult!.feedbacks!.length === 0) { +

+ } + @if (assessmentResult && assessmentResult!.feedbacks && assessmentResult!.feedbacks!.length > 0) { +

+ + + + + + + + @if (assessmentsNames) { + + @for (feedback of referencedFeedback; track feedback) { + + + + + } + + } +
+ @if (feedback.reference) { + {{ assessmentsNames[feedback.referenceId!]?.type }} + } + @if (feedback.reference) { + {{ assessmentsNames[feedback.referenceId!]?.name }} + } + @if (feedback.reference) { +
+ } + @if (feedback.text || feedback.detailText || feedback.gradingInstruction) { + Feedback: + } +
+ {{ feedback.credits | number: '1.0-1' }} + @if (feedback.isSubsequent) { + + } +
+ } +
+ } +
diff --git a/src/main/webapp/app/exercises/modeling/participate/modeling-submission.component.ts b/src/main/webapp/app/exercises/modeling/participate/modeling-submission.component.ts index e9c54a294c31..b77b05f098dd 100644 --- a/src/main/webapp/app/exercises/modeling/participate/modeling-submission.component.ts +++ b/src/main/webapp/app/exercises/modeling/participate/modeling-submission.component.ts @@ -22,6 +22,7 @@ import { modelingTour } from 'app/guided-tour/tours/modeling-tour'; import { ParticipationWebsocketService } from 'app/overview/participation-websocket.service'; import { ButtonType } from 'app/shared/components/button.component'; import { AUTOSAVE_CHECK_INTERVAL, AUTOSAVE_EXERCISE_INTERVAL, AUTOSAVE_TEAM_EXERCISE_INTERVAL } from 'app/shared/constants/exercise-exam-constants'; +import { faTimeline } from '@fortawesome/free-solid-svg-icons'; import { ComponentCanDeactivate } from 'app/shared/guard/can-deactivate.model'; import { stringifyIgnoringFields } from 'app/shared/util/utils'; import { Subject, Subscription, TeardownLogic } from 'rxjs'; @@ -33,9 +34,11 @@ import { Course } from 'app/entities/course.model'; import { AssessmentNamesForModelId, getNamesForAssessments } from '../assess/modeling-assessment.util'; import { faExclamationTriangle, faGripLines } from '@fortawesome/free-solid-svg-icons'; import { faListAlt } from '@fortawesome/free-regular-svg-icons'; -import { onError } from 'app/shared/util/global.utils'; import { SubmissionPatch } from 'app/entities/submission-patch.model'; import { AssessmentType } from 'app/entities/assessment-type.model'; +import { catchError, filter, skip, switchMap, tap } from 'rxjs/operators'; +import { onError } from 'app/shared/util/global.utils'; +import { of } from 'rxjs'; @Component({ selector: 'jhi-modeling-submission', @@ -61,12 +64,15 @@ export class ModelingSubmissionComponent implements OnInit, OnDestroy, Component @Input() isExamSummary = false; private subscription: Subscription; - private resultUpdateListener: Subscription; + private manualResultUpdateListener: Subscription; + private athenaResultUpdateListener: Subscription; participation: StudentParticipation; isOwnerOfParticipation: boolean; modelingExercise: ModelingExercise; + modelingParticipationHeader: StudentParticipation; + modelingExerciseHeader: ModelingExercise; course?: Course; result?: Result; resultWithComplaint?: Result; @@ -75,6 +81,9 @@ export class ModelingSubmissionComponent implements OnInit, OnDestroy, Component selectedRelationships: string[]; submission: ModelingSubmission; + submissionId: number | undefined; + sortedSubmissionHistory: ModelingSubmission[]; + sortedResultHistory: Result[]; assessmentResult?: Result; assessmentsNames: AssessmentNamesForModelId = {}; @@ -96,6 +105,7 @@ export class ModelingSubmissionComponent implements OnInit, OnDestroy, Component isAfterAssessmentDueDate: boolean; isLoading: boolean; isLate: boolean; // indicates if the submission is late + isGeneratingFeedback: boolean; ComplaintType = ComplaintType; examMode = false; @@ -111,6 +121,11 @@ export class ModelingSubmissionComponent implements OnInit, OnDestroy, Component faGripLines = faGripLines; farListAlt = faListAlt; faExclamationTriangle = faExclamationTriangle; + faTimeline = faTimeline; + + // mode + isFeedbackView: boolean = false; + showResultHistory: boolean = false; constructor( private jhiWebsocketService: JhiWebsocketService, @@ -132,23 +147,30 @@ export class ModelingSubmissionComponent implements OnInit, OnDestroy, Component if (this.inputValuesArePresent()) { this.setupComponentWithInputValues(); } else { - this.subscription = this.route.params.subscribe((params) => { - const participationId = params['participationId'] ?? this.participationId; - - if (participationId) { - this.modelingSubmissionService.getLatestSubmissionForModelingEditor(participationId).subscribe({ - next: (modelingSubmission) => { + this.route.params + .pipe( + switchMap((params) => { + this.participationId = params['participationId'] ?? this.participationId; + this.submissionId = Number(params['submissionId']) || undefined; + this.isFeedbackView = !!this.submissionId; + + // If participationId exists and feedback view is needed, fetch history results first + if (this.participationId && this.isFeedbackView) { + return this.fetchSubmissionHistory().pipe(switchMap(() => this.fetchLatestSubmission())); + } + // Otherwise, directly fetch the latest submission + return this.fetchLatestSubmission(); + }), + ) + .subscribe({ + next: (modelingSubmission) => { + if (modelingSubmission) { this.updateModelingSubmission(modelingSubmission); - if (this.modelingExercise.teamMode) { - this.setupSubmissionStreamForTeam(); - } else { - this.setAutoSaveTimer(); - } - }, - error: (error: HttpErrorResponse) => onError(this.alertService, error), - }); - } - }); + this.setupMode(); + } + }, + error: (error) => onError(this.alertService, error), + }); } const isDisplayedOnExamSummaryPage = !this.displayHeader && this.participationId !== undefined; @@ -157,6 +179,60 @@ export class ModelingSubmissionComponent implements OnInit, OnDestroy, Component } } + private setupMode(): void { + if (this.modelingExercise.teamMode) { + this.setupSubmissionStreamForTeam(); + } else { + this.setAutoSaveTimer(); + } + } + + private fetchLatestSubmission() { + return this.modelingSubmissionService.getLatestSubmissionForModelingEditor(this.participationId!).pipe( + catchError((error: HttpErrorResponse) => { + onError(this.alertService, error); + return of(null); // Return null on error + }), + ); + } + + // Fetch the results and sort them + // Fetch the submissions and sort them by the latest result's completionDate in descending order + private fetchSubmissionHistory() { + return this.modelingSubmissionService.getSubmissionsWithResultsForParticipation(this.participationId!).pipe( + catchError((error: HttpErrorResponse) => { + onError(this.alertService, error); + return of([]); + }), + tap((submissions: ModelingSubmission[]) => { + this.sortedSubmissionHistory = submissions.sort((a, b) => { + // Get the latest result for each submission (sorted by completionDate descending) + const latestResultA = this.sortResultsByCompletionDate(a.results ?? [])[0]; + const latestResultB = this.sortResultsByCompletionDate(b.results ?? [])[0]; + + // Use the latest result's completionDate for comparison + const dateA = latestResultA?.completionDate ? latestResultA.completionDate.valueOf() : 0; + const dateB = latestResultB?.completionDate ? latestResultB.completionDate.valueOf() : 0; + + return dateB - dateA; // Sort submissions by latest result's completionDate in descending order + }); + this.sortedResultHistory = this.sortedSubmissionHistory.map((submission) => { + const result = getLatestSubmissionResult(submission)!; + result.participation = submission.participation; + return result; + }); + }), + ); + } + + private sortResultsByCompletionDate(results: Result[]): Result[] { + return results.sort((a, b) => { + const dateA = a.completionDate ? a.completionDate.valueOf() : 0; + const dateB = b.completionDate ? b.completionDate.valueOf() : 0; + return dateB - dateA; // Descending + }); + } + private inputValuesArePresent(): boolean { return !!(this.inputExercise || this.inputSubmission || this.inputParticipation); } @@ -189,11 +265,28 @@ export class ModelingSubmissionComponent implements OnInit, OnDestroy, Component /** * Updates the modeling submission with the given modeling submission. */ - private updateModelingSubmission(modelingSubmission: ModelingSubmission) { + private updateModelingSubmission(modelingSubmission: ModelingSubmission): void { if (!modelingSubmission) { this.alertService.error('artemisApp.apollonDiagram.submission.noSubmission'); } + // In the header we always want to display the latest submission, even when we are viewing a specific submission + this.modelingParticipationHeader = modelingSubmission.participation as StudentParticipation; + this.modelingParticipationHeader.submissions = [omit(modelingSubmission, 'participation')]; + this.modelingExerciseHeader = this.modelingParticipationHeader.exercise as ModelingExercise; + this.modelingExerciseHeader.studentParticipations = [this.participation]; + + // If isFeedbackView is true and submissionId is present, we want to find the corresponding submission and not get the latest one + if (this.isFeedbackView && this.submissionId && this.sortedSubmissionHistory) { + const matchingSubmission = this.sortedSubmissionHistory.find((submission) => submission.id === this.submissionId); + + if (matchingSubmission) { + modelingSubmission = matchingSubmission; + } else { + console.warn(`Submission with ID ${this.submissionId} not found in sorted history results.`); + } + } + this.submission = modelingSubmission; // reconnect participation <--> result @@ -227,15 +320,23 @@ export class ModelingSubmissionComponent implements OnInit, OnDestroy, Component } this.explanation = this.submission.explanationText ?? ''; this.subscribeToWebsockets(); - if (getLatestSubmissionResult(this.submission) && this.isAfterAssessmentDueDate) { + if ((getLatestSubmissionResult(this.submission) && this.isAfterAssessmentDueDate) || this.isFeedbackView) { this.result = getLatestSubmissionResult(this.submission); + if (this.isFeedbackView && this.submissionId) { + this.result = this.sortedSubmissionHistory.find((submission) => submission.id === this.submissionId)?.latestResult; + } } this.resultWithComplaint = getFirstResultWithComplaint(this.submission); if (this.submission.submitted && this.result && this.result.completionDate) { - this.modelingAssessmentService.getAssessment(this.submission.id!).subscribe((assessmentResult: Result) => { - this.assessmentResult = assessmentResult; + if (!this.isFeedbackView) { + this.modelingAssessmentService.getAssessment(this.submission.id!).subscribe((assessmentResult: Result) => { + this.assessmentResult = assessmentResult; + this.prepareAssessmentData(); + }); + } else if (this.result) { + this.assessmentResult = this.modelingAssessmentService.convertResult(this.result!); this.prepareAssessmentData(); - }); + } } this.isLoading = false; this.guidedTourService.enableTourForExercise(this.modelingExercise, modelingTour, true); @@ -289,19 +390,61 @@ export class ModelingSubmissionComponent implements OnInit, OnDestroy, Component * and show the new assessment information to the student. */ private subscribeToNewResultsWebsocket(): void { - if (!this.participation || !this.participation.id) { + if (!this.participation?.id) { return; } - this.resultUpdateListener = this.participationWebsocketService.subscribeForLatestResultOfParticipation(this.participation.id, true).subscribe((newResult: Result) => { - if (newResult && newResult.completionDate) { - this.assessmentResult = newResult; - this.assessmentResult = this.modelingAssessmentService.convertResult(newResult); - this.prepareAssessmentData(); - if (this.assessmentResult.assessmentType !== AssessmentType.AUTOMATIC_ATHENA) { - this.alertService.info('artemisApp.modelingEditor.newAssessment'); - } + + const resultStream$ = this.participationWebsocketService.subscribeForLatestResultOfParticipation(this.participation.id, true); + + // Handle initial results (no skip) + this.manualResultUpdateListener = resultStream$ + .pipe( + filter((result): result is Result => !!result), + filter((result) => !result.assessmentType || result.assessmentType !== AssessmentType.AUTOMATIC_ATHENA), + ) + .subscribe(this.handleManualAssessment.bind(this)); + + // Handle Athena results (with skip) + this.athenaResultUpdateListener = resultStream$ + .pipe( + skip(1), + filter((result): result is Result => !!result), + filter((result) => result.assessmentType === AssessmentType.AUTOMATIC_ATHENA), + ) + .subscribe(this.handleAthenaAssessment.bind(this)); + } + + /** + * Handles manual assessments (non-Athena). Converts the result, prepares the assessment data, and informs the user of a new assessment. + * @param result - The result of the assessment. + */ + private handleManualAssessment(result: Result): void { + if (!result.completionDate) { + return; + } + + this.assessmentResult = this.modelingAssessmentService.convertResult(result); + this.prepareAssessmentData(); + this.alertService.info('artemisApp.modelingEditor.newAssessment'); + } + + /** + * Handles Athena assessments. Converts the result, prepares the assessment data, and provides feedback based on the result's success or failure. + * @param result - The result of the Athena assessment. + */ + private handleAthenaAssessment(result: Result): void { + if (result.completionDate) { + this.assessmentResult = this.modelingAssessmentService.convertResult(result); + this.prepareAssessmentData(); + + if (result.successful) { + this.alertService.success('artemisApp.exercise.athenaFeedbackSuccessful'); } - }); + } else if (result.successful === false) { + this.alertService.error('artemisApp.exercise.athenaFeedbackFailed'); + } + + this.isGeneratingFeedback = false; } /** @@ -422,10 +565,12 @@ export class ModelingSubmissionComponent implements OnInit, OnDestroy, Component this.submissionChange.next(this.submission); this.participation = this.submission.participation as StudentParticipation; this.participation.exercise = this.modelingExercise; + this.modelingParticipationHeader = this.submission.participation as StudentParticipation; // reconnect so that the submission status is displayed correctly in the result.component this.submission.participation!.submissions = [this.submission]; this.participationWebsocketService.addParticipation(this.participation, this.modelingExercise); this.modelingExercise.studentParticipations = [this.participation]; + this.modelingExerciseHeader.studentParticipations = [this.participation]; this.result = getLatestSubmissionResult(this.submission); this.retryStarted = false; @@ -505,8 +650,11 @@ export class ModelingSubmissionComponent implements OnInit, OnDestroy, Component if (this.automaticSubmissionWebsocketChannel) { this.jhiWebsocketService.unsubscribe(this.automaticSubmissionWebsocketChannel); } - if (this.resultUpdateListener) { - this.resultUpdateListener.unsubscribe(); + if (this.manualResultUpdateListener) { + this.manualResultUpdateListener.unsubscribe(); + } + if (this.athenaResultUpdateListener) { + this.athenaResultUpdateListener.unsubscribe(); } } @@ -532,6 +680,14 @@ export class ModelingSubmissionComponent implements OnInit, OnDestroy, Component return undefined; } + /* + * Check if the latest submission has an Athena result + */ + get hasAthenaResultForLatestSubmission(): boolean { + const latestResult = getLatestSubmissionResult(this.submission); + return latestResult?.assessmentType === AssessmentType.AUTOMATIC_ATHENA; + } + /** * Updates the model of the submission with the current Apollon model state * and the explanation text of submission with current explanation if explanation is defined @@ -674,4 +830,6 @@ export class ModelingSubmissionComponent implements OnInit, OnDestroy, Component return 'entity.action.submitDueDateMissedTooltip'; } + + protected readonly hasExerciseDueDatePassed = hasExerciseDueDatePassed; } diff --git a/src/main/webapp/app/exercises/modeling/participate/modeling-submission.service.ts b/src/main/webapp/app/exercises/modeling/participate/modeling-submission.service.ts index c2c12426f973..9da063181dbe 100644 --- a/src/main/webapp/app/exercises/modeling/participate/modeling-submission.service.ts +++ b/src/main/webapp/app/exercises/modeling/participate/modeling-submission.service.ts @@ -132,4 +132,17 @@ export class ModelingSubmissionService { .get(`api/participations/${participationId}/latest-modeling-submission`, { responseType: 'json' }) .pipe(map((res: ModelingSubmission) => this.submissionService.convertSubmissionFromServer(res))); } + + /** + * Get all submissions with results for a participation + * @param {number} participationId - Id of the participation + */ + getSubmissionsWithResultsForParticipation(participationId: number): Observable { + const url = `api/participations/${participationId}/submissions-with-results`; + return this.http.get(url).pipe( + map((submissions: ModelingSubmission[]) => { + return submissions.map((submission) => this.submissionService.convertSubmissionFromServer(submission) as ModelingSubmission); + }), + ); + } } diff --git a/src/main/webapp/app/exercises/programming/assess/code-editor-tutor-assessment-container.component.html b/src/main/webapp/app/exercises/programming/assess/code-editor-tutor-assessment-container.component.html index 2945fec0662b..9382ac324adf 100644 --- a/src/main/webapp/app/exercises/programming/assess/code-editor-tutor-assessment-container.component.html +++ b/src/main/webapp/app/exercises/programming/assess/code-editor-tutor-assessment-container.component.html @@ -98,12 +98,18 @@ + @if (isAtLeastEditor && localVCEnabled && !isTestRun) { + + + + + } - @if (!localVCEnabled) { + @if (isAtLeastEditor && !localVCEnabled) { { // Get all files with content from template repository @@ -194,6 +198,7 @@ export class CodeEditorTutorAssessmentContainerComponent implements OnInit, OnDe const observable = this.repositoryFileService.getFilesWithContent(); // Set back to student participation this.domainService.setDomain([DomainType.PARTICIPATION, this.participation]); + this.localRepositoryLink = getLocalRepositoryLink(this.courseId, this.exerciseId, this.participation.id!, this.exerciseGroupId, this.examId); return observable; }), tap((templateFilesObj) => { diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-build-configuration/programming-exercise-build-configuration.component.html b/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-build-configuration/programming-exercise-build-configuration.component.html index 8460ca15d6fd..bb59ed44df0d 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-build-configuration/programming-exercise-build-configuration.component.html +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-build-configuration/programming-exercise-build-configuration.component.html @@ -16,7 +16,57 @@ />
@if (!isAeolus()) { -
+ @if (isLanguageSupported) { +
+ +
+ @if (isNetworkDisabled) { + + } + } + +
+ + + + + + + + + + + + + + + + + + + + +
+
- @if (quizExercise) { -
-
- @for (question of quizExercise.quizQuestions; track question; let i = $index) { + + +
+
+
+ @for (question of quizExercise.quizQuestions; track question; let index = $index) {
- @if (question.type === DRAG_AND_DROP) { - - DD - - } - @if (question.type === MULTIPLE_CHOICE) { - - MC - + @switch (question.type) { + @case (DRAG_AND_DROP) { + + } + @case (MULTIPLE_CHOICE) { + + } + @case (SHORT_ANSWER) { + + } } - @if (question.type === SHORT_ANSWER) { + {{ 'artemisApp.quizExercise.explanationAnswered' | artemisTranslate }} + {{ 'artemisApp.quizExercise.explanationNotAnswered' | artemisTranslate }} + - SA + {{ abbreviation }} - } - {{ 'artemisApp.quizExercise.explanationAnswered' | artemisTranslate }} - {{ 'artemisApp.quizExercise.explanationNotAnswered' | artemisTranslate }} +
}
+ +
- @for (question of quizExercise.quizQuestions; track question; let i = $index) { + @if (!waitingForQuizStart) { + @if (!submission.submitted && !showingResult && remainingTimeSeconds >= 0) { +

+ } + @if (submission.submitted && !showingResult) { +

+ } + @if (showingResult && mode !== 'solution') { +

+ } + } + @for (question of quizExercise.quizQuestions; track question; let index = $index) {
@if (question.type === MULTIPLE_CHOICE) { [submittedResult]="result" [quizQuestions]="quizExercise.quizQuestions" [forceSampleSolution]="mode === 'solution'" - [questionIndex]="i + 1" + [questionIndex]="index + 1" [score]="questionScores[question.id!]" /> } @if (question.type === DRAG_AND_DROP) { [clickDisabled]="submission.submitted || remainingTimeSeconds < 0" [showResult]="showingResult" [forceSampleSolution]="mode === 'solution'" - [questionIndex]="i + 1" + [questionIndex]="index + 1" [score]="questionScores[question.id!]" /> } @if (question.type === SHORT_ANSWER) { [clickDisabled]="submission.submitted || remainingTimeSeconds < 0" [showResult]="showingResult" [forceSampleSolution]="mode === 'solution'" - [questionIndex]="i + 1" + [questionIndex]="index + 1" [score]="questionScores[question.id!]" /> }
}
- } -
- @if (quizExercise) { -