diff --git a/README.md b/README.md index d3acf73cc0c4..c8f5bafa3eaf 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.2.war +./artemis-server-cli deploy username@artemistest.ase.in.tum.de -w build/libs/Artemis-7.7.3.war ``` ## Architecture diff --git a/build.gradle b/build.gradle index a9f811243528..1dd978e9ddb8 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ plugins { } group = "de.tum.cit.aet.artemis" -version = "7.7.2" +version = "7.7.3" description = "Interactive Learning with Individual Feedback" java { @@ -345,7 +345,6 @@ dependencies { // 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}") @@ -377,7 +376,7 @@ dependencies { implementation "javax.cache:cache-api:1.1.1" implementation "org.hibernate.orm:hibernate-core:${hibernate_version}" - implementation "com.zaxxer:HikariCP:6.1.0" + implementation "com.zaxxer:HikariCP:6.2.1" implementation "org.apache.commons:commons-text:1.12.0" implementation "org.apache.commons:commons-math3:3.6.1" @@ -414,8 +413,13 @@ dependencies { implementation "org.springframework.cloud:spring-cloud-starter-config:4.1.3" implementation "org.springframework.cloud:spring-cloud-commons:4.1.4" - implementation "io.netty:netty-all:4.1.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.retry:spring-retry:2.0.10" @@ -424,6 +428,7 @@ 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" @@ -472,7 +477,7 @@ dependencies { implementation "com.google.code.gson:gson:2.11.0" - implementation "com.google.errorprone:error_prone_annotations:2.35.1" + implementation "com.google.errorprone:error_prone_annotations:2.36.0" // NOTE: we want to keep the same unique version for all configurations, implementation and annotationProcessor implementation("net.bytebuddy:byte-buddy") { @@ -539,13 +544,21 @@ dependencies { testImplementation "io.github.classgraph:classgraph:4.8.179" testImplementation "org.awaitility:awaitility:4.2.2" testImplementation "org.apache.maven.shared:maven-invoker:3.3.0" - testImplementation "org.gradle:gradle-tooling-api:8.11" + 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") { 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" @@ -597,7 +610,7 @@ tasks.withType(Test).configureEach { afterTest { descriptor, result -> if (result.resultType == TestResult.ResultType.FAILURE) { - String failedTest = "${descriptor.className}::${descriptor.name}" + var failedTest = "${descriptor.className}::${descriptor.name}" logger.debug("Adding " + failedTest + " to failedTests...") failedTests << [failedTest] } diff --git a/docs/dev/setup/docker-compose.rst b/docs/dev/setup/docker-compose.rst index a655f44e6ab7..0ed185bab82f 100644 --- a/docs/dev/setup/docker-compose.rst +++ b/docs/dev/setup/docker-compose.rst @@ -21,7 +21,7 @@ Getting Started with Docker Compose Make sure that Docker Desktop has enough memory (~ 6GB). To adapt it, go to ``Settings -> Resources``. 2. Check that all local network ports used by Docker Compose are free (e.g. you haven't started a local MySQL server - when you would like to start a Docker Compose instance of mysql) + when you would like to start a Docker Compose instance of MySQL). 3. Run ``docker compose pull && docker compose up`` in the directory ``docker/`` 4. Open the Artemis instance in your browser at https://localhost 5. Run ``docker compose down`` in the directory ``docker/`` to stop and remove the docker containers @@ -62,13 +62,12 @@ Three example commands to run such setups: .. code:: bash - docker compose -f docker/atlassian.yml up docker compose -f docker/mysql.yml -f docker/gitlab-jenkins.yml up docker compose -f docker/artemis-dev-postgres.yml up .. tip:: There is also a single ``docker-compose.yml`` in the directory ``docker/`` which mirrors the setup of ``artemis-prod-mysql.yml``. - This should provide a quick way, without manual changes necessary, for new contributors to startup an Artemis instance. + This should provide a quick way, without manual changes necessary, for new contributors to start up an Artemis instance. If the documentation just mentions to run ``docker compose`` without a ``-f `` argument, it's assumed you are running the command from the ``docker/`` directory. @@ -82,7 +81,7 @@ is defined in the following files: * ``gitlab.yml``: **GitLab Service** * ``jenkins.yml``: **Jenkins Service** -For testing mails or SAML logins, you can append the following services to any setup with an artemis container: +For testing mails or SAML logins, you can append the following services to any setup with an Artemis container: * ``mailhog.yml``: **Mailhog Service** (email testing tool) * ``saml-test.yml``: **Saml-Test Service** (SAML Test Identity Provider for testing SAML features) @@ -145,7 +144,7 @@ Get a shell into the containers ``docker compose exec artemis-app bash`` or if the container is not yet running: ``docker compose run --rm artemis-app bash`` - mysql container: - ``docker compose exec mysql bash`` or directly into mysql ``docker compose exec mysql mysql`` + ``docker compose exec mysql bash`` or directly into MySQL ``docker compose exec mysql mysql`` Analog for other services. @@ -157,7 +156,7 @@ Other useful commands - Stop, remove containers and volumes: ``docker compose down -v`` - Remove Artemis-related volumes/state: ``docker volume rm artemis-data artemis-mysql-data`` - This is helpful in setups where you just want to delete the state of artemis + This is helpful in setups where you just want to delete the state of Artemis but not of Jenkins and GitLab for instance. - Stop a service: ``docker compose stop `` (restart via ``docker compose start ``) diff --git a/docs/dev/setup/jenkins-gitlab.rst b/docs/dev/setup/jenkins-gitlab.rst index 3adbbabac478..8f117cff3cca 100644 --- a/docs/dev/setup/jenkins-gitlab.rst +++ b/docs/dev/setup/jenkins-gitlab.rst @@ -890,7 +890,8 @@ and the corresponding Docker image can be found on .. code:: bash - docker compose -f docker/.yml up --build -d + docker compose -f docker/.yml build --no-cache + docker compose -f docker/.yml up -d 3. Build the new Docker image: diff --git a/docs/user/exams/student/access_exam.png b/docs/user/exams/student/access_exam.png index 76a0dddd02f3..a3579e5fc874 100644 Binary files a/docs/user/exams/student/access_exam.png and b/docs/user/exams/student/access_exam.png differ diff --git a/docs/user/exams/student/buttons/exam_hand_in_early.png b/docs/user/exams/student/buttons/exam_hand_in_early.png index d443c4116554..266d30608fbf 100644 Binary files a/docs/user/exams/student/buttons/exam_hand_in_early.png and b/docs/user/exams/student/buttons/exam_hand_in_early.png differ diff --git a/docs/user/exams/student/buttons/save_exercise.png b/docs/user/exams/student/buttons/save_exercise.png new file mode 100644 index 000000000000..7e16a5f622b5 Binary files /dev/null and b/docs/user/exams/student/buttons/save_exercise.png differ diff --git a/docs/user/exams/student/buttons/upload.png b/docs/user/exams/student/buttons/upload.png new file mode 100644 index 000000000000..cbe7b44079f7 Binary files /dev/null and b/docs/user/exams/student/buttons/upload.png differ diff --git a/docs/user/exams/student/exam_bar.png b/docs/user/exams/student/exam_bar.png index d1a4728bcc57..02914123a784 100644 Binary files a/docs/user/exams/student/exam_bar.png and b/docs/user/exams/student/exam_bar.png differ diff --git a/docs/user/exams/student/exam_modeling_exercises.png b/docs/user/exams/student/exam_modeling_exercises.png index 037cb07283aa..8fdd0912935e 100644 Binary files a/docs/user/exams/student/exam_modeling_exercises.png and b/docs/user/exams/student/exam_modeling_exercises.png differ diff --git a/docs/user/exams/student/exam_navigation_sidebar.png b/docs/user/exams/student/exam_navigation_sidebar.png index e8455e426df5..f6a45add9523 100644 Binary files a/docs/user/exams/student/exam_navigation_sidebar.png and b/docs/user/exams/student/exam_navigation_sidebar.png differ diff --git a/docs/user/exams/student/exam_overview.png b/docs/user/exams/student/exam_overview.png index eab66ecf60a7..84a85532187c 100644 Binary files a/docs/user/exams/student/exam_overview.png and b/docs/user/exams/student/exam_overview.png differ diff --git a/docs/user/exams/student/exam_programming_exercises.png b/docs/user/exams/student/exam_programming_exercises.png index b2018403e57a..1956f4fd173d 100644 Binary files a/docs/user/exams/student/exam_programming_exercises.png and b/docs/user/exams/student/exam_programming_exercises.png differ diff --git a/docs/user/exams/student/exam_quiz_exercises.png b/docs/user/exams/student/exam_quiz_exercises.png index 32c8d3634610..0c4bed59d3f6 100644 Binary files a/docs/user/exams/student/exam_quiz_exercises.png and b/docs/user/exams/student/exam_quiz_exercises.png differ diff --git a/docs/user/exams/student/exam_text_exercises.png b/docs/user/exams/student/exam_text_exercises.png index f81591045eb6..d79126ed398f 100644 Binary files a/docs/user/exams/student/exam_text_exercises.png and b/docs/user/exams/student/exam_text_exercises.png differ diff --git a/docs/user/exams/students_guide.rst b/docs/user/exams/students_guide.rst index b38278b75d22..f56f5fa94894 100644 --- a/docs/user/exams/students_guide.rst +++ b/docs/user/exams/students_guide.rst @@ -105,6 +105,14 @@ Welcome Screen Welcome Screen, waiting for exam start +This video offers a detailed guide on accessing your exams: + +.. raw:: html + + + Exam Conduction ^^^^^^^^^^^^^^^ - Once the exam working time starts and you have confirmed your participation, the *Exercise Overview* screen will appear. This screen lists all exercises that are part of your exam with their respective amount of points, title and exercise type. The status column indicates the status of each exercise and whether you have a submission in them or not. @@ -132,6 +140,14 @@ Exam Conduction Exam Navigation Sidebar +- You have two options to save your changes for an exercise: + + 1. Click the |save_exercise| button to manually save and submit your changes. + 2. Select an exercise in the navigation sidebar (either the current one or a different exercise), which will automatically save and submit your changes. + + .. warning:: + The |save_exercise| button is only available for text, modeling, and quiz exercises. For file upload exercises, you need to manually click the |upload| button, and for programming exercises, you need to manually click the |submit| button to save and submit your changes. + - On the header, you will find the exam bar that includes the remaining time and the |exam_hand_in_early| button. If you click this button, you will be sent to the exam `End Screen`_. - The *time left* until the end of the exam is also shown next to the button. @@ -373,6 +389,14 @@ Summary Complaining about the Assessment of a Text Exercise +This video offers a detailed guide on participating in your exams: + +.. raw:: html + + + Example Solutions ^^^^^^^^^^^^^^^^^ - If the instructor sets the example solution publication date of the exam, the solutions will be available after that date. @@ -470,3 +494,5 @@ Grades .. |exam_no_results_found| image:: student/buttons/exam_no_results_found.png .. |exam_hand_in_early| image:: student/buttons/exam_hand_in_early.png .. |saved_exercises| image:: student/buttons/saved_exercises.png +.. |upload| image:: student/buttons/upload.png +.. |save_exercise| image:: student/buttons/save_exercise.png diff --git a/gradle.properties b/gradle.properties index 2f2ade9049ba..b234044bcc8f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,8 +7,8 @@ npm_version=10.8.0 # Dependency versions jhipster_dependencies_version=8.7.2 -spring_boot_version=3.3.5 -spring_security_version=6.3.4 +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 hibernate_version=6.4.10.Final # TODO: can we update to 5.x? @@ -25,18 +25,20 @@ jplag_version=5.1.0 # NOTE: we do not need to use the latest version 9.x here as long as Stanford CoreNLP does not reference it lucene_version=8.11.4 slf4j_version=2.0.16 -sentry_version=7.17.0 +sentry_version=7.18.0 liquibase_version=4.30.0 docker_java_version=3.4.0 logback_version=1.5.12 java_parser_version=3.26.2 byte_buddy_version=1.15.10 +netty_version=4.1.115.Final # testing # make sure both versions are compatible -junit_version=5.11.0 +junit_version=5.11.3 junit_platform_version=1.11.3 mockito_version=5.14.2 +testcontainer_version=1.20.4 # gradle plugin version diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 94113f200e61..e2847c820046 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/package-lock.json b/package-lock.json index 4362969bff33..cc5fa3584980 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "artemis", - "version": "7.7.2", + "version": "7.7.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "artemis", - "version": "7.7.2", + "version": "7.7.3", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -26,14 +26,14 @@ "@danielmoncada/angular-datetime-picker": "18.1.0", "@fingerprintjs/fingerprintjs": "4.5.1", "@fortawesome/angular-fontawesome": "0.15.0", - "@fortawesome/fontawesome-svg-core": "6.6.0", - "@fortawesome/free-regular-svg-icons": "6.6.0", - "@fortawesome/free-solid-svg-icons": "6.6.0", + "@fortawesome/fontawesome-svg-core": "6.7.1", + "@fortawesome/free-regular-svg-icons": "6.7.1", + "@fortawesome/free-solid-svg-icons": "6.7.1", "@ls1intum/apollon": "3.3.15", "@ng-bootstrap/ng-bootstrap": "17.0.1", "@ngx-translate/core": "16.0.3", "@ngx-translate/http-loader": "16.0.0", - "@sentry/angular": "8.38.0", + "@sentry/angular": "8.39.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.0", + "dompurify": "3.2.1", "emoji-js": "3.8.0", "export-to-csv": "1.4.0", "fast-json-patch": "3.1.1", @@ -66,7 +66,7 @@ "papaparse": "5.4.1", "pdf-lib": "1.17.1", "pdfjs-dist": "4.8.69", - "posthog-js": "1.186.0", + "posthog-js": "1.187.2", "rxjs": "7.8.1", "simple-statistics": "7.8.7", "smoothscroll-polyfill": "0.4.4", @@ -83,15 +83,15 @@ "devDependencies": { "@angular-builders/jest": "18.0.0", "@angular-devkit/build-angular": "18.2.12", - "@angular-eslint/builder": "18.4.0", - "@angular-eslint/eslint-plugin": "18.4.0", - "@angular-eslint/eslint-plugin-template": "18.4.0", - "@angular-eslint/schematics": "18.4.0", - "@angular-eslint/template-parser": "18.4.0", + "@angular-eslint/builder": "18.4.1", + "@angular-eslint/eslint-plugin": "18.4.1", + "@angular-eslint/eslint-plugin-template": "18.4.1", + "@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.38.0", + "@sentry/types": "8.39.0", "@types/crypto-js": "4.2.2", "@types/d3-shape": "3.1.6", "@types/dompurify": "3.0.5", @@ -99,29 +99,29 @@ "@types/jest": "29.5.14", "@types/lodash-es": "4.17.12", "@types/markdown-it": "14.1.2", - "@types/node": "22.9.0", + "@types/node": "22.9.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.14.0", - "@typescript-eslint/parser": "8.14.0", - "eslint": "9.14.0", + "@typescript-eslint/eslint-plugin": "8.15.0", + "@typescript-eslint/parser": "8.15.0", + "eslint": "9.15.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-deprecation": "3.0.0", "eslint-plugin-jest": "28.9.0", "eslint-plugin-jest-extended": "2.4.0", "eslint-plugin-prettier": "5.2.1", "folder-hash": "4.0.4", - "husky": "9.1.6", + "husky": "9.1.7", "jest": "29.7.0", "jest-canvas-mock": "2.5.2", "jest-date-mock": "1.0.10", "jest-extended": "4.0.2", "jest-fail-on-console": "3.3.1", "jest-junit": "16.0.0", - "jest-preset-angular": "14.2.4", + "jest-preset-angular": "14.3.2", "lint-staged": "15.2.10", "ng-mocks": "14.13.1", "ngxtension": "4.1.0", @@ -462,9 +462,9 @@ } }, "node_modules/@angular-eslint/builder": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-18.4.0.tgz", - "integrity": "sha512-FOzGHX/nHSV1wSduSsabsx3aqC1nfde0opEpEDSOJhxExDxKCwoS1XPy1aERGyKip4ZVA6phC3dLtoBH3QMkVQ==", + "version": "18.4.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-18.4.1.tgz", + "integrity": "sha512-Ofkwd9Rg52K+AgvnV1RXYXVBGJvl5jD7+4dqwoprqXG7YKNTdHy5vqNZ5XDSMb26qjoZF7JC+IKruKFaON/ZaA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -473,21 +473,21 @@ } }, "node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-18.4.0.tgz", - "integrity": "sha512-HlFHt2qgdd+jqyVIkCXmrjHauXo/XY3Rp0UNabk83ejGi/raM/6lEFI7iFWzHxLyiAKk4OgGI5W26giSQw991A==", + "version": "18.4.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-18.4.1.tgz", + "integrity": "sha512-gCQC0mgBO1bwHDXL9CUgHW+Rf1XGZCLAopoXnggwxGkBCx+oww507t+jrSOxdh+4OTKU4ZfmbtWd7Y8AeXns8w==", "dev": true, "license": "MIT" }, "node_modules/@angular-eslint/eslint-plugin": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-18.4.0.tgz", - "integrity": "sha512-Saz9lkWPN3da7ZKW17UsOSN7DeY+TPh+wz/6GCNZCh67Uw2wvMC9agb+4hgpZNXYCP5+u7erqzxQmBoWnS/A+A==", + "version": "18.4.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-18.4.1.tgz", + "integrity": "sha512-FoHwj+AFo8ONKb8wEK5qpo6uefuyklZlDqErJxeC3fpNIJzDe8PWBcJsuZt7Wwm/HeggWgt0Au6h+3IEa0V3BQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "18.4.0", - "@angular-eslint/utils": "18.4.0" + "@angular-eslint/bundled-angular-compiler": "18.4.1", + "@angular-eslint/utils": "18.4.1" }, "peerDependencies": { "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", @@ -496,14 +496,14 @@ } }, "node_modules/@angular-eslint/eslint-plugin-template": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-18.4.0.tgz", - "integrity": "sha512-n3uZFCy76DnggPqjSVFV3gYD1ik7jCG28o2/HO4kobcMNKnwW8XAlFUagQ4TipNQh7fQiAefsEqvv2quMsYDVw==", + "version": "18.4.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-18.4.1.tgz", + "integrity": "sha512-sofnKpi6wOZ6avVfYYqB7sCgGgWF2HgCZfW+IAp1MtVD2FBa1zTSbbfIZ1I8Akpd22UXa4LKJd0TLwm5XHHkiQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "18.4.0", - "@angular-eslint/utils": "18.4.0", + "@angular-eslint/bundled-angular-compiler": "18.4.1", + "@angular-eslint/utils": "18.4.1", "aria-query": "5.3.2", "axobject-query": "4.1.0" }, @@ -515,15 +515,15 @@ } }, "node_modules/@angular-eslint/schematics": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-18.4.0.tgz", - "integrity": "sha512-ssqe+0YCfekbWIXNdCrHfoPK/bPZAWybs0Bn/b99dfd8h8uyXkERo9AzIOx4Uyj/08SkP9aPL/0uOOEHDsRGwQ==", + "version": "18.4.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-18.4.1.tgz", + "integrity": "sha512-1+gGodwh+UevtEx9mzZbzP1uY/9NAGEbsn8jisG1TEPDby2wKScQj6U6JwGxoW/Dd/4SIeSdilruZPALkqha7g==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/eslint-plugin": "18.4.0", - "@angular-eslint/eslint-plugin-template": "18.4.0", - "ignore": "5.3.2", + "@angular-eslint/eslint-plugin": "18.4.1", + "@angular-eslint/eslint-plugin-template": "18.4.1", + "ignore": "6.0.2", "semver": "7.6.3", "strip-json-comments": "3.1.1" }, @@ -532,14 +532,24 @@ "@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.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-18.4.0.tgz", - "integrity": "sha512-VTep3Xd3IOaRIPL+JN/TV4/2DqUPbjtF3TNY15diD/llnrEhqFnmsvMihexbQyTqzOG+zU554oK44YfvAtHOrw==", + "version": "18.4.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-18.4.1.tgz", + "integrity": "sha512-LsStXVyso/89gQU5eiJebB/b1j+wrRtTLjk+ODVUTa7NGCCT7B7xI6ToTchkBEpSTHLT9pEQXHsHer3FymsQRQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "18.4.0", + "@angular-eslint/bundled-angular-compiler": "18.4.1", "eslint-scope": "^8.0.2" }, "peerDependencies": { @@ -548,13 +558,13 @@ } }, "node_modules/@angular-eslint/utils": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-18.4.0.tgz", - "integrity": "sha512-At1yS8GRviGBoaupiQwEOL4/IcZJCE/+2vpXdItMWPGB1HWetxlKAUZTMmIBX/r5Z7CoXxl+LbqpGhrhyzIQAg==", + "version": "18.4.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-18.4.1.tgz", + "integrity": "sha512-F5UGE1J/CRmTbl8vjexQRwRglNqnJwdXCUejaG+qlGssSHoWcRB+ubbR/na3PdnzeJdBE6DkLYElXnOQZ6YKfg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "18.4.0" + "@angular-eslint/bundled-angular-compiler": "18.4.1" }, "peerDependencies": { "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", @@ -3427,9 +3437,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", - "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.0.tgz", + "integrity": "sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3466,9 +3476,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz", - "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.0.tgz", + "integrity": "sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3476,9 +3486,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", + "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", "dev": true, "license": "MIT", "dependencies": { @@ -3561,9 +3571,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.14.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.14.0.tgz", - "integrity": "sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==", + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.15.0.tgz", + "integrity": "sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==", "dev": true, "license": "MIT", "engines": { @@ -3616,45 +3626,45 @@ } }, "node_modules/@fortawesome/fontawesome-common-types": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz", - "integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==", + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.1.tgz", + "integrity": "sha512-gbDz3TwRrIPT3i0cDfujhshnXO9z03IT1UKRIVi/VEjpNHtSBIP2o5XSm+e816FzzCFEzAxPw09Z13n20PaQJQ==", "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/@fortawesome/fontawesome-svg-core": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz", - "integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==", + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.1.tgz", + "integrity": "sha512-8dBIHbfsKlCk2jHQ9PoRBg2Z+4TwyE3vZICSnoDlnsHA6SiMlTwfmW6yX0lHsRmWJugkeb92sA0hZdkXJhuz+g==", "license": "MIT", "dependencies": { - "@fortawesome/fontawesome-common-types": "6.6.0" + "@fortawesome/fontawesome-common-types": "6.7.1" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-regular-svg-icons": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.6.0.tgz", - "integrity": "sha512-Yv9hDzL4aI73BEwSEh20clrY8q/uLxawaQ98lekBx6t9dQKDHcDzzV1p2YtBGTtolYtNqcWdniOnhzB+JPnQEQ==", + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.1.tgz", + "integrity": "sha512-e13cp+bAx716RZOTQ59DhqikAgETA9u1qTBHO3e3jMQQ+4H/N1NC1ZVeFYt1V0m+Th68BrEL1/X6XplISutbXg==", "license": "(CC-BY-4.0 AND MIT)", "dependencies": { - "@fortawesome/fontawesome-common-types": "6.6.0" + "@fortawesome/fontawesome-common-types": "6.7.1" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-solid-svg-icons": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz", - "integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==", + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.1.tgz", + "integrity": "sha512-BTKc0b0mgjWZ2UDKVgmwaE0qt0cZs6ITcDgjrti5f/ki7aF5zs+N91V6hitGo3TItCFtnKg6cUVGdTmBFICFRg==", "license": "(CC-BY-4.0 AND MIT)", "dependencies": { - "@fortawesome/fontawesome-common-types": "6.6.0" + "@fortawesome/fontawesome-common-types": "6.7.1" }, "engines": { "node": ">=6" @@ -6265,73 +6275,73 @@ } }, "node_modules/@sentry-internal/browser-utils": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.38.0.tgz", - "integrity": "sha512-5QMVcssrAcmjKT0NdFYcX0b0wwZovGAZ9L2GajErXtHkBenjI2sgR2+5J7n+QZGuk2SC1qhGmT1O9i3p3UEwew==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.39.0.tgz", + "integrity": "sha512-5jcO3os1aQIMNZptniMUCCkZ3KOvyUPSyrQeGB7NxhJoieIwmopo5qIXyeRLHu0htL7H7A1gPYln6Ji3d/KUUA==", "license": "MIT", "dependencies": { - "@sentry/core": "8.38.0", - "@sentry/types": "8.38.0", - "@sentry/utils": "8.38.0" + "@sentry/core": "8.39.0", + "@sentry/types": "8.39.0", + "@sentry/utils": "8.39.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/feedback": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.38.0.tgz", - "integrity": "sha512-AW5HCCAlc3T1jcSuNhbFVNO1CHyJ5g5tsGKEP4VKgu+D1Gg2kZ5S2eFatLBUP/BD5JYb1A7p6XPuzYp1XfMq0A==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.39.0.tgz", + "integrity": "sha512-V5J/tnzAK8bXdXQzY7lnlYMqfTKgI+9BD7L7oHxQnDUzlShsV14xFGZVhEbPsjYficdIN9wpoYIyWDxwrFX1Qg==", "license": "MIT", "dependencies": { - "@sentry/core": "8.38.0", - "@sentry/types": "8.38.0", - "@sentry/utils": "8.38.0" + "@sentry/core": "8.39.0", + "@sentry/types": "8.39.0", + "@sentry/utils": "8.39.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/replay": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.38.0.tgz", - "integrity": "sha512-mQPShKnIab7oKwkwrRxP/D8fZYHSkDY+cvqORzgi+wAwgnunytJQjz9g6Ww2lJu98rHEkr5SH4V4rs6PZYZmnQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.39.0.tgz", + "integrity": "sha512-1IEXhg2XuKC1hx/Pf5p2L7McKjQPfVOWyQhjNUH2mHWbpOyvc1BhZoZKCgbbspwOAVuvj4n40PvOVyjfzU5Yew==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "8.38.0", - "@sentry/core": "8.38.0", - "@sentry/types": "8.38.0", - "@sentry/utils": "8.38.0" + "@sentry-internal/browser-utils": "8.39.0", + "@sentry/core": "8.39.0", + "@sentry/types": "8.39.0", + "@sentry/utils": "8.39.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/replay-canvas": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.38.0.tgz", - "integrity": "sha512-OxmlWzK9J8mRM+KxdSnQ5xuxq+p7TiBzTz70FT3HltxmeugvDkyp6803UcFqHOPHR35OYeVLOalym+FmvNn9kw==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.39.0.tgz", + "integrity": "sha512-NCp4E60SFfg9pXdMgcdpctYENFOvJ58UPGllGjO3xpYoMkd4DGZQp947Tgw9hATTCDnyYNIy5v/zYbDV4Wbw3w==", "license": "MIT", "dependencies": { - "@sentry-internal/replay": "8.38.0", - "@sentry/core": "8.38.0", - "@sentry/types": "8.38.0", - "@sentry/utils": "8.38.0" + "@sentry-internal/replay": "8.39.0", + "@sentry/core": "8.39.0", + "@sentry/types": "8.39.0", + "@sentry/utils": "8.39.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry/angular": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@sentry/angular/-/angular-8.38.0.tgz", - "integrity": "sha512-FBeokQllQwFArdtQ8OMIHatIa1MOj3nJEQjHCuuUgK4ys0vpX/ithPuHU1lEpd1qkUGUnHYHyjjQW6QLY3whwg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@sentry/angular/-/angular-8.39.0.tgz", + "integrity": "sha512-yke0NULFosz4Fap9NGKTVzRKoJRx8+sAC8jA2qdU49SUtxon+L3LN5D6QbE402kdMWEscxKa1cHrgfIvJfOZZA==", "license": "MIT", "dependencies": { - "@sentry/browser": "8.38.0", - "@sentry/core": "8.38.0", - "@sentry/types": "8.38.0", - "@sentry/utils": "8.38.0", + "@sentry/browser": "8.39.0", + "@sentry/core": "8.39.0", + "@sentry/types": "8.39.0", + "@sentry/utils": "8.39.0", "tslib": "^2.4.1" }, "engines": { @@ -6345,52 +6355,52 @@ } }, "node_modules/@sentry/browser": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.38.0.tgz", - "integrity": "sha512-AZR+b0EteNZEGv6JSdBD22S9VhQ7nrljKsSnzxobBULf3BpwmhmCzTbDrqWszKDAIDYmL+yQJIR2glxbknneWQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.39.0.tgz", + "integrity": "sha512-Xpqh84MnqoFID0owbugTeq/3QXgNwc3EdHAN/HFUdxEAyJS4j7Wi1DIBXN+ZRzMYX3m2QHOAymCWjnFtv+H8WQ==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "8.38.0", - "@sentry-internal/feedback": "8.38.0", - "@sentry-internal/replay": "8.38.0", - "@sentry-internal/replay-canvas": "8.38.0", - "@sentry/core": "8.38.0", - "@sentry/types": "8.38.0", - "@sentry/utils": "8.38.0" + "@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" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry/core": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.38.0.tgz", - "integrity": "sha512-sGD+5TEHU9G7X7zpyaoJxpOtwjTjvOd1f/MKBrWW2vf9UbYK+GUJrOzLhMoSWp/pHSYgvObkJkDb/HwieQjvhQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.39.0.tgz", + "integrity": "sha512-rg2mHtwdCaedqub7bd+ht08vZgtwPO7el5m5sPNeb7V75GcQwSziu6G02vGxCBCsAHpoFn1A+0JLEajaYzZI7w==", "license": "MIT", "dependencies": { - "@sentry/types": "8.38.0", - "@sentry/utils": "8.38.0" + "@sentry/types": "8.39.0", + "@sentry/utils": "8.39.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry/types": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.38.0.tgz", - "integrity": "sha512-fP5H9ZX01W4Z/EYctk3mkSHi7d06cLcX2/UWqwdWbyPWI+pL2QpUPICeO/C+8SnmYx//wFj3qWDhyPCh1PdFAA==", + "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.38.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.38.0.tgz", - "integrity": "sha512-3X7MgIKIx+2q5Al7QkhaRB4wV6DvzYsaeIwdqKUzGLuRjXmNgJrLoU87TAwQRmZ6Wr3IoEpThZZMNrzYPXxArw==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.39.0.tgz", + "integrity": "sha512-pIBnr/cROds92CcYWBW3z1zFH4uJkMPL2AxEv/ZcLg/NTb1Okz/ZaDP+NMzUfzriYvFBOFk0wPk0h5sYx6Umqw==", "license": "MIT", "dependencies": { - "@sentry/types": "8.38.0" + "@sentry/types": "8.39.0" }, "engines": { "node": ">=14.18" @@ -7081,9 +7091,9 @@ } }, "node_modules/@types/node": { - "version": "22.9.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", - "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", + "version": "22.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.1.tgz", + "integrity": "sha512-p8Yy/8sw1caA8CdRIQBG5tiLHmxtQKObCijiAa9Ez+d4+PRffM4054xbju0msf+cvhJpnFEeNjxmVT/0ipktrg==", "dev": true, "license": "MIT", "dependencies": { @@ -7229,7 +7239,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/turndown": { @@ -7287,17 +7297,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.14.0.tgz", - "integrity": "sha512-tqp8H7UWFaZj0yNO6bycd5YjMwxa6wIHOLZvWPkidwbgLCsBMetQoGj7DPuAlWa2yGO3H48xmPwjhsSPPCGU5w==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.15.0.tgz", + "integrity": "sha512-+zkm9AR1Ds9uLWN3fkoeXgFppaQ+uEVtfOV62dDmsy9QCNqlRHWNEck4yarvRNrvRcHQLGfqBNui3cimoz8XAg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.14.0", - "@typescript-eslint/type-utils": "8.14.0", - "@typescript-eslint/utils": "8.14.0", - "@typescript-eslint/visitor-keys": "8.14.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", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -7321,16 +7331,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.14.0.tgz", - "integrity": "sha512-2p82Yn9juUJq0XynBXtFCyrBDb6/dJombnz6vbo6mgQEtWHfvHbQuEa9kAOVIt1c9YFwi7H6WxtPj1kg+80+RA==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.15.0.tgz", + "integrity": "sha512-7n59qFpghG4uazrF9qtGKBZXn7Oz4sOMm8dwNWDQY96Xlm2oX67eipqcblDj+oY1lLCbf1oltMZFpUso66Kl1A==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.14.0", - "@typescript-eslint/types": "8.14.0", - "@typescript-eslint/typescript-estree": "8.14.0", - "@typescript-eslint/visitor-keys": "8.14.0", + "@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", "debug": "^4.3.4" }, "engines": { @@ -7350,14 +7360,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.14.0.tgz", - "integrity": "sha512-aBbBrnW9ARIDn92Zbo7rguLnqQ/pOrUguVpbUwzOhkFg2npFDwTgPGqFqE0H5feXcOoJOfX3SxlJaKEVtq54dw==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.15.0.tgz", + "integrity": "sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.14.0", - "@typescript-eslint/visitor-keys": "8.14.0" + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7368,14 +7378,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.14.0.tgz", - "integrity": "sha512-Xcz9qOtZuGusVOH5Uk07NGs39wrKkf3AxlkK79RBK6aJC1l03CobXjJbwBPSidetAOV+5rEVuiT1VSBUOAsanQ==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.14.0", - "@typescript-eslint/utils": "8.14.0", + "@typescript-eslint/typescript-estree": "8.15.0", + "@typescript-eslint/utils": "8.15.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -7386,6 +7396,9 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, "peerDependenciesMeta": { "typescript": { "optional": true @@ -7393,9 +7406,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.14.0.tgz", - "integrity": "sha512-yjeB9fnO/opvLJFAsPNYlKPnEM8+z4og09Pk504dkqonT02AyL5Z9SSqlE0XqezS93v6CXn49VHvB2G7XSsl0g==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.15.0.tgz", + "integrity": "sha512-n3Gt8Y/KyJNe0S3yDCD2RVKrHBC4gTUcLTebVBXacPy091E6tNspFLKRXlk3hwT4G55nfr1n2AdFqi/XMxzmPQ==", "dev": true, "license": "MIT", "engines": { @@ -7407,14 +7420,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.14.0.tgz", - "integrity": "sha512-OPXPLYKGZi9XS/49rdaCbR5j/S14HazviBlUQFvSKz3npr3NikF+mrgK7CFVur6XEt95DZp/cmke9d5i3vtVnQ==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.15.0.tgz", + "integrity": "sha512-1eMp2JgNec/niZsR7ioFBlsh/Fk0oJbhaqO0jRyQBMgkz7RrFfkqF9lYYmBoGBaSiLnu8TAPQTwoTUiSTUW9dg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.14.0", - "@typescript-eslint/visitor-keys": "8.14.0", + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -7436,16 +7449,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.14.0.tgz", - "integrity": "sha512-OGqj6uB8THhrHj0Fk27DcHPojW7zKwKkPmHXHvQ58pLYp4hy8CSUdTKykKeh+5vFqTTVmjz0zCOOPKRovdsgHA==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.15.0.tgz", + "integrity": "sha512-k82RI9yGhr0QM3Dnq+egEpz9qB6Un+WLYhmoNcvl8ltMEededhh7otBVVIDDsEEttauwdY/hQoSsOv13lxrFzQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.14.0", - "@typescript-eslint/types": "8.14.0", - "@typescript-eslint/typescript-estree": "8.14.0" + "@typescript-eslint/scope-manager": "8.15.0", + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/typescript-estree": "8.15.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7456,17 +7469,22 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.14.0.tgz", - "integrity": "sha512-vG0XZo8AdTH9OE6VFRwAZldNc7qtJ/6NLGWak+BtENuEUXGZgFpihILPiBvKXvJ2nFu27XNGC6rKiwuaoMbYzQ==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.15.0.tgz", + "integrity": "sha512-h8vYOulWec9LhpwfAdZf2bjr8xIp0KNKnpgqSz0qqYYKAW/QZKw3ktRndbiAtUz4acH4QLQavwZBYCc0wulA/Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.14.0", - "eslint-visitor-keys": "^3.4.3" + "@typescript-eslint/types": "8.15.0", + "eslint-visitor-keys": "^4.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7476,6 +7494,19 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@vitejs/plugin-basic-ssl": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.1.0.tgz", @@ -10322,10 +10353,13 @@ } }, "node_modules/dompurify": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.0.tgz", - "integrity": "sha512-AMdOzK44oFWqHEi0wpOqix/fUNY707OmoeFDnbi3Q5I8uOpy21ufUA5cDJPr0bosxrflOVD/H2DMSvuGKJGfmQ==", - "license": "(MPL-2.0 OR Apache-2.0)" + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.1.tgz", + "integrity": "sha512-NBHEsc0/kzRYQd+AY6HR6B/IgsqzBABrqJbpCDQII/OK6h7B7LXzweZTDsqSW2LkTRpoxf18YUP+YjGySk6B3w==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } }, "node_modules/domutils": { "version": "3.1.0", @@ -10703,27 +10737,27 @@ } }, "node_modules/eslint": { - "version": "9.14.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.14.0.tgz", - "integrity": "sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==", + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.15.0.tgz", + "integrity": "sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.18.0", - "@eslint/core": "^0.7.0", - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.14.0", - "@eslint/plugin-kit": "^0.2.0", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.9.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.15.0", + "@eslint/plugin-kit": "^0.2.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.0", + "@humanwhocodes/retry": "^0.4.1", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.5", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", @@ -10742,8 +10776,7 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" @@ -12667,9 +12700,9 @@ } }, "node_modules/husky": { - "version": "9.1.6", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.6.tgz", - "integrity": "sha512-sqbjZKK7kf44hfdE94EoX8MZNk0n7HeW37O4YrVGCF4wzgQjp+akPAkfUK5LZ6KuR/6sqeAVuXHji+RzQgOn5A==", + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", "dev": true, "license": "MIT", "bin": { @@ -13870,9 +13903,9 @@ } }, "node_modules/jest-preset-angular": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/jest-preset-angular/-/jest-preset-angular-14.2.4.tgz", - "integrity": "sha512-xyhkaiBdn3keBgxxkcbqZu/my3ADU9NcDrz6DaMuGRaxz/bf6ZC1qxZ1eQuz5V1WuA3/rD64VA3Kke8P6E9qNg==", + "version": "14.3.2", + "resolved": "https://registry.npmjs.org/jest-preset-angular/-/jest-preset-angular-14.3.2.tgz", + "integrity": "sha512-Aoei1O/o7x1I6bSCpU08jGqtQ2RBq7HvNbMIo/vHHbM50v4HX1gF3sWZTkM0U0KorNkdwZeONjMsPNwHyUAKqA==", "dev": true, "license": "MIT", "dependencies": { @@ -17506,9 +17539,9 @@ "license": "MIT" }, "node_modules/posthog-js": { - "version": "1.186.0", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.186.0.tgz", - "integrity": "sha512-WagGNrDtvyOhmX1Gtf1hJQMBy1mB1vx9gtC6BKEfJi2pvEFtQuAzQ9c/tMUTmY0o2ZF5ZBFiZ2IRs4kbFLMvPQ==", + "version": "1.187.2", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.187.2.tgz", + "integrity": "sha512-IGKsZ7M4AYACm5I6gGGFrv9kR/MOnVYw11XFYCLk363n0nq+ghwenoW1jJVL9gZLGKiMsConUR8rG2DD2OMKyg==", "license": "MIT", "dependencies": { "core-js": "^3.38.1", @@ -20149,13 +20182,6 @@ "node": "*" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, "node_modules/thingies": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", @@ -20907,9 +20933,9 @@ } }, "node_modules/vite": { - "version": "5.4.10", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.10.tgz", - "integrity": "sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==", + "version": "5.4.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", + "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 2ca02dacf336..c26e50b915ce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "artemis", - "version": "7.7.2", + "version": "7.7.3", "description": "Interactive Learning with Individual Feedback", "private": true, "license": "MIT", @@ -29,14 +29,14 @@ "@danielmoncada/angular-datetime-picker": "18.1.0", "@fingerprintjs/fingerprintjs": "4.5.1", "@fortawesome/angular-fontawesome": "0.15.0", - "@fortawesome/fontawesome-svg-core": "6.6.0", - "@fortawesome/free-regular-svg-icons": "6.6.0", - "@fortawesome/free-solid-svg-icons": "6.6.0", + "@fortawesome/fontawesome-svg-core": "6.7.1", + "@fortawesome/free-regular-svg-icons": "6.7.1", + "@fortawesome/free-solid-svg-icons": "6.7.1", "@ls1intum/apollon": "3.3.15", "@ng-bootstrap/ng-bootstrap": "17.0.1", "@ngx-translate/core": "16.0.3", "@ngx-translate/http-loader": "16.0.0", - "@sentry/angular": "8.38.0", + "@sentry/angular": "8.39.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.0", + "dompurify": "3.2.1", "emoji-js": "3.8.0", "export-to-csv": "1.4.0", "fast-json-patch": "3.1.1", @@ -69,7 +69,7 @@ "papaparse": "5.4.1", "pdf-lib": "1.17.1", "pdfjs-dist": "4.8.69", - "posthog-js": "1.186.0", + "posthog-js": "1.187.2", "rxjs": "7.8.1", "simple-statistics": "7.8.7", "smoothscroll-polyfill": "0.4.4", @@ -92,14 +92,14 @@ "d3-transition": "^3.0.1" }, "@typescript-eslint/utils": { - "eslint": "^9.14.0" + "eslint": "^9.15.0" }, "braces": "3.0.3", "cookie": "1.0.1", "critters": "0.0.25", "debug": "4.3.7", "eslint-plugin-deprecation": { - "eslint": "^9.14.0" + "eslint": "^9.15.0" }, "express": "5.0.1", "jsdom": "25.0.1", @@ -107,7 +107,7 @@ "rimraf": "6.0.1", "semver": "7.6.3", "tough-cookie": "5.0.0", - "vite": "5.4.10", + "vite": "5.4.11", "webpack-dev-middleware": "7.4.2", "webpack-dev-server": "5.1.0", "word-wrap": "1.2.5", @@ -117,15 +117,15 @@ "devDependencies": { "@angular-builders/jest": "18.0.0", "@angular-devkit/build-angular": "18.2.12", - "@angular-eslint/builder": "18.4.0", - "@angular-eslint/eslint-plugin": "18.4.0", - "@angular-eslint/eslint-plugin-template": "18.4.0", - "@angular-eslint/schematics": "18.4.0", - "@angular-eslint/template-parser": "18.4.0", + "@angular-eslint/builder": "18.4.1", + "@angular-eslint/eslint-plugin": "18.4.1", + "@angular-eslint/eslint-plugin-template": "18.4.1", + "@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.38.0", + "@sentry/types": "8.39.0", "@types/crypto-js": "4.2.2", "@types/d3-shape": "3.1.6", "@types/dompurify": "3.0.5", @@ -133,29 +133,29 @@ "@types/jest": "29.5.14", "@types/lodash-es": "4.17.12", "@types/markdown-it": "14.1.2", - "@types/node": "22.9.0", + "@types/node": "22.9.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.14.0", - "@typescript-eslint/parser": "8.14.0", - "eslint": "9.14.0", + "@typescript-eslint/eslint-plugin": "8.15.0", + "@typescript-eslint/parser": "8.15.0", + "eslint": "9.15.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-deprecation": "3.0.0", "eslint-plugin-jest": "28.9.0", "eslint-plugin-jest-extended": "2.4.0", "eslint-plugin-prettier": "5.2.1", "folder-hash": "4.0.4", - "husky": "9.1.6", + "husky": "9.1.7", "jest": "29.7.0", "jest-canvas-mock": "2.5.2", "jest-date-mock": "1.0.10", "jest-extended": "4.0.2", "jest-fail-on-console": "3.3.1", "jest-junit": "16.0.0", - "jest-preset-angular": "14.2.4", + "jest-preset-angular": "14.3.2", "lint-staged": "15.2.10", "ngxtension": "4.1.0", "ng-mocks": "14.13.1", diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CompetencyService.java b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CompetencyService.java index 705c162c6341..3b0a4bc083c9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CompetencyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CompetencyService.java @@ -51,8 +51,7 @@ public CompetencyService(CompetencyRepository competencyRepository, Authorizatio LearningObjectImportService learningObjectImportService, CompetencyLectureUnitLinkRepository competencyLectureUnitLinkRepository, CourseRepository courseRepository, CompetencyExerciseLinkRepository competencyExerciseLinkRepository) { super(competencyProgressRepository, courseCompetencyRepository, competencyRelationRepository, competencyProgressService, exerciseService, lectureUnitService, - learningPathService, authCheckService, standardizedCompetencyRepository, lectureUnitCompletionRepository, learningObjectImportService, - competencyLectureUnitLinkRepository, courseRepository); + learningPathService, authCheckService, standardizedCompetencyRepository, lectureUnitCompletionRepository, learningObjectImportService, courseRepository); this.competencyRepository = competencyRepository; this.competencyExerciseLinkRepository = competencyExerciseLinkRepository; } 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 cbe33e70b710..88cad15f1000 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 @@ -30,7 +30,6 @@ import de.tum.cit.aet.artemis.atlas.dto.CompetencyRelationDTO; import de.tum.cit.aet.artemis.atlas.dto.CompetencyWithTailRelationDTO; import de.tum.cit.aet.artemis.atlas.dto.UpdateCourseCompetencyRelationDTO; -import de.tum.cit.aet.artemis.atlas.repository.CompetencyLectureUnitLinkRepository; import de.tum.cit.aet.artemis.atlas.repository.CompetencyProgressRepository; import de.tum.cit.aet.artemis.atlas.repository.CompetencyRelationRepository; import de.tum.cit.aet.artemis.atlas.repository.CourseCompetencyRepository; @@ -83,15 +82,13 @@ public class CourseCompetencyService { private final LearningObjectImportService learningObjectImportService; - private final CompetencyLectureUnitLinkRepository competencyLectureUnitLinkRepository; - private final CourseRepository courseRepository; public CourseCompetencyService(CompetencyProgressRepository competencyProgressRepository, CourseCompetencyRepository courseCompetencyRepository, CompetencyRelationRepository competencyRelationRepository, CompetencyProgressService competencyProgressService, ExerciseService exerciseService, LectureUnitService lectureUnitService, LearningPathService learningPathService, AuthorizationCheckService authCheckService, StandardizedCompetencyRepository standardizedCompetencyRepository, LectureUnitCompletionRepository lectureUnitCompletionRepository, - LearningObjectImportService learningObjectImportService, CompetencyLectureUnitLinkRepository competencyLectureUnitLinkRepository, CourseRepository courseRepository) { + LearningObjectImportService learningObjectImportService, CourseRepository courseRepository) { this.competencyProgressRepository = competencyProgressRepository; this.courseCompetencyRepository = courseCompetencyRepository; this.competencyRelationRepository = competencyRelationRepository; @@ -103,7 +100,6 @@ public CourseCompetencyService(CompetencyProgressRepository competencyProgressRe this.standardizedCompetencyRepository = standardizedCompetencyRepository; this.lectureUnitCompletionRepository = lectureUnitCompletionRepository; this.learningObjectImportService = learningObjectImportService; - this.competencyLectureUnitLinkRepository = competencyLectureUnitLinkRepository; this.courseRepository = courseRepository; } diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/PrerequisiteService.java b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/PrerequisiteService.java index eb66a98d641f..96a68280f334 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/PrerequisiteService.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/PrerequisiteService.java @@ -43,8 +43,7 @@ public PrerequisiteService(PrerequisiteRepository prerequisiteRepository, Author StandardizedCompetencyRepository standardizedCompetencyRepository, CourseCompetencyRepository courseCompetencyRepository, ExerciseService exerciseService, LearningObjectImportService learningObjectImportService, CompetencyLectureUnitLinkRepository competencyLectureUnitLinkRepository, CourseRepository courseRepository) { super(competencyProgressRepository, courseCompetencyRepository, competencyRelationRepository, competencyProgressService, exerciseService, lectureUnitService, - learningPathService, authCheckService, standardizedCompetencyRepository, lectureUnitCompletionRepository, learningObjectImportService, - competencyLectureUnitLinkRepository, courseRepository); + learningPathService, authCheckService, standardizedCompetencyRepository, lectureUnitCompletionRepository, learningObjectImportService, courseRepository); this.prerequisiteRepository = prerequisiteRepository; } diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildLogDTO.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildLogDTO.java new file mode 100644 index 000000000000..62e9e2229a88 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildLogDTO.java @@ -0,0 +1,13 @@ +package de.tum.cit.aet.artemis.buildagent.dto; + +import java.io.Serializable; +import java.time.ZonedDateTime; + +import jakarta.validation.constraints.NotNull; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record BuildLogDTO(@NotNull ZonedDateTime time, @NotNull String log) implements Serializable { + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildResult.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildResult.java index fbb9bbbdbee1..f8fec009f4b9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildResult.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildResult.java @@ -27,6 +27,7 @@ // in the future are migrated or cleared. Changes should be communicated in release notes as potentially breaking changes. @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_EMPTY) +// TODO: this should be a record in the future public class BuildResult extends AbstractBuildResultNotificationDTO implements Serializable { private final String assignmentRepoBranchName; @@ -41,7 +42,7 @@ public class BuildResult extends AbstractBuildResultNotificationDTO implements S private final List jobs; - private List buildLogEntries = new ArrayList<>(); + private List buildLogEntries = new ArrayList<>(); private final List staticCodeAnalysisReports; @@ -123,7 +124,8 @@ public boolean hasLogs() { @Override public List extractBuildLogs() { - return buildLogEntries; + // convert the buildLogEntry DTOs to BuildLogEntry objects + return buildLogEntries.stream().map(log -> new BuildLogEntry(log.time(), log.log())).toList(); } /** @@ -131,7 +133,7 @@ public List extractBuildLogs() { * * @param buildLogEntries the buildLogEntries to be set */ - public void setBuildLogEntries(List buildLogEntries) { + public void setBuildLogEntries(List buildLogEntries) { this.buildLogEntries = buildLogEntries; hasLogs = true; } diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/ResultQueueItem.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/ResultQueueItem.java index 9ed86f7306f7..6964d2cd58a8 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/ResultQueueItem.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/ResultQueueItem.java @@ -5,11 +5,8 @@ import com.fasterxml.jackson.annotation.JsonInclude; -import de.tum.cit.aet.artemis.programming.domain.build.BuildLogEntry; - // NOTE: this data structure is used in shared code between core and build agent nodes. Changing it requires that the shared data structures in Hazelcast (or potentially Redis) // in the future are migrated or cleared. Changes should be communicated in release notes as potentially breaking changes. @JsonInclude(JsonInclude.Include.NON_EMPTY) -// TODO: this data structure should not use BuildLogEntry because it's an entity class (and not a DTO) -public record ResultQueueItem(BuildResult buildResult, BuildJobQueueItem buildJobQueueItem, List buildLogs, Throwable exception) implements Serializable { +public record ResultQueueItem(BuildResult buildResult, BuildJobQueueItem buildJobQueueItem, List buildLogs, Throwable exception) implements Serializable { } diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildAgentDockerService.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildAgentDockerService.java index 378bd12ed247..c7aca819eb91 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildAgentDockerService.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildAgentDockerService.java @@ -180,16 +180,14 @@ public static class MyPullImageResultCallback extends PullImageResultCallback { @Override public void onNext(PullResponseItem item) { String msg = "~~~~~~~~~~~~~~~~~~~~ Pull image progress: " + item.getStatus() + " ~~~~~~~~~~~~~~~~~~~~"; - log.info(msg); - buildLogsMap.appendBuildLogEntry(buildJobId, msg); + log.debug(msg); super.onNext(item); } @Override public void onComplete() { String msg = "~~~~~~~~~~~~~~~~~~~~ Pull image complete ~~~~~~~~~~~~~~~~~~~~"; - log.info(msg); - buildLogsMap.appendBuildLogEntry(buildJobId, msg); + log.debug(msg); super.onComplete(); } } 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 5d4c72d5b491..b6f4c63019da 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 @@ -43,9 +43,9 @@ import com.github.dockerjava.api.model.Frame; import com.github.dockerjava.api.model.HostConfig; +import de.tum.cit.aet.artemis.buildagent.dto.BuildLogDTO; import de.tum.cit.aet.artemis.core.exception.LocalCIException; import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage; -import de.tum.cit.aet.artemis.programming.domain.build.BuildLogEntry; import de.tum.cit.aet.artemis.programming.service.ci.ContinuousIntegrationService.RepositoryCheckoutPath; /** @@ -414,7 +414,7 @@ private void executeDockerCommand(String containerId, String buildJobId, boolean @Override public void onNext(Frame item) { String text = new String(item.getPayload()); - BuildLogEntry buildLogEntry = new BuildLogEntry(ZonedDateTime.now(), text); + BuildLogDTO buildLogEntry = new BuildLogDTO(ZonedDateTime.now(), text); if (buildJobId != null) { buildLogsMap.appendBuildLogEntry(buildJobId, buildLogEntry); } 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 75bbaf826b00..fd06f24e09f7 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 @@ -345,7 +345,7 @@ private BuildResult runScriptAndParseResults(BuildJobQueueItem buildJob, String try { buildResult = parseTestResults(testResultsTarInputStream, buildJob.buildConfig().branch(), assignmentRepoCommitHash, testRepoCommitHash, buildCompletedDate, buildJob.id()); - buildResult.setBuildLogEntries(buildLogsMap.getBuildLogs(buildJob.id())); + buildResult.setBuildLogEntries(buildLogsMap.getAndTruncateBuildLogs(buildJob.id())); } catch (IOException | IllegalStateException e) { msg = "Error while parsing test results"; 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 551b20f8bdd9..be480892b8e3 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 @@ -32,9 +32,9 @@ import com.hazelcast.topic.ITopic; import de.tum.cit.aet.artemis.buildagent.dto.BuildJobQueueItem; +import de.tum.cit.aet.artemis.buildagent.dto.BuildLogDTO; import de.tum.cit.aet.artemis.buildagent.dto.BuildResult; import de.tum.cit.aet.artemis.core.exception.LocalCIException; -import de.tum.cit.aet.artemis.programming.domain.build.BuildLogEntry; /** * This service is responsible for adding build jobs to the Integrated Code Lifecycle executor service. @@ -171,7 +171,7 @@ public CompletableFuture executeBuildJob(BuildJobQueueItem buildJob finishCancelledBuildJob(buildJobItem.repositoryInfo().assignmentRepositoryUri(), buildJobItem.id(), containerName); String msg = "Build job with id " + buildJobItem.id() + " was cancelled."; String stackTrace = stackTraceToString(e); - buildLogsMap.appendBuildLogEntry(buildJobItem.id(), new BuildLogEntry(ZonedDateTime.now(), msg + "\n" + stackTrace)); + buildLogsMap.appendBuildLogEntry(buildJobItem.id(), new BuildLogDTO(ZonedDateTime.now(), msg + "\n" + stackTrace)); throw new CompletionException(msg, e); } else { @@ -232,7 +232,7 @@ private CompletableFuture createCompletableFuture(Supplier> buildLogsMap = new ConcurrentHashMap<>(); + @Value("${artemis.continuous-integration.build-logs.max-lines-per-job:10000}") + private int maxLogLinesPerBuildJob; - public List getBuildLogs(String buildLogId) { - return buildLogsMap.get(buildLogId); + @Value("${artemis.continuous-integration.build-logs.max-chars-per-line:1024}") + private int maxCharsPerLine; + + // buildJobId --> List of build logs + private final ConcurrentMap> buildLogsMap = new ConcurrentHashMap<>(); + + /** + * Appends a new build log entry to the build logs for the specified build job ID. + * + * @param buildJobId the ID of the build job to append a log message to + * @param message the message to append to the build log + */ + public void appendBuildLogEntry(String buildJobId, String message) { + appendBuildLogEntry(buildJobId, new BuildLogDTO(ZonedDateTime.now(), message + "\n")); } - public void appendBuildLogEntry(String buildLogId, String message) { - appendBuildLogEntry(buildLogId, new BuildLogEntry(ZonedDateTime.now(), message + "\n")); + /** + * Appends a new build log entry to the build logs for the specified build job ID. + * Only the first maxCharsPerLine characters of the log message will be appended. Longer characters will be truncated to avoid memory issues. + * Only the first maxLogLinesPerBuildJob log entries will be stored. Newer logs will be ignored to avoid memory issues + * + * @param buildJobId the ID of the build job to append a log message to + * @param buildLog the build log entry to append to the build log + */ + public void appendBuildLogEntry(String buildJobId, BuildLogDTO buildLog) { + List buildLogs = buildLogsMap.computeIfAbsent(buildJobId, k -> new ArrayList<>()); + if (buildLogs.size() < maxLogLinesPerBuildJob) { + if (buildLog.log() != null && buildLog.log().length() > maxCharsPerLine) { + buildLog = new BuildLogDTO(buildLog.time(), buildLog.log().substring(0, maxCharsPerLine) + "\n"); + } + buildLogs.add(buildLog); + } } - public void appendBuildLogEntry(String buildLogId, BuildLogEntry buildLog) { - buildLogsMap.computeIfAbsent(buildLogId, k -> new ArrayList<>()).add(buildLog); + public void removeBuildLogs(String buildJobId) { + buildLogsMap.remove(buildJobId); } - public void removeBuildLogs(String buildLogId) { - buildLogsMap.remove(buildLogId); + /** + * Retrieves and truncates the build logs for the specified build job ID. Does not modify the original build logs. + * + * @param buildJobId the ID of the build job to retrieve and truncate + * @return a list of truncated build log entries, or null if no logs are found for the specified ID + */ + public List getAndTruncateBuildLogs(String buildJobId) { + List buildLogs = buildLogsMap.get(buildJobId); + + if (buildLogs == null) { + return null; + } + + // Truncate the build logs to maxLogLinesPerBuildJob + if (buildLogs.size() > maxLogLinesPerBuildJob) { + List truncatedBuildLogs = new ArrayList<>(buildLogs.subList(0, maxLogLinesPerBuildJob)); + truncatedBuildLogs.add(new BuildLogDTO(ZonedDateTime.now(), "Truncated build logs...\n")); + buildLogs = truncatedBuildLogs; + } + + return buildLogs; } } diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/SharedQueueProcessingService.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/SharedQueueProcessingService.java index 0823ec5a4f9b..2c2e8b8e16bb 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/SharedQueueProcessingService.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/SharedQueueProcessingService.java @@ -49,11 +49,11 @@ import de.tum.cit.aet.artemis.buildagent.dto.BuildAgentDTO; import de.tum.cit.aet.artemis.buildagent.dto.BuildAgentInformation; import de.tum.cit.aet.artemis.buildagent.dto.BuildJobQueueItem; +import de.tum.cit.aet.artemis.buildagent.dto.BuildLogDTO; import de.tum.cit.aet.artemis.buildagent.dto.BuildResult; import de.tum.cit.aet.artemis.buildagent.dto.JobTimingInfo; import de.tum.cit.aet.artemis.buildagent.dto.ResultQueueItem; import de.tum.cit.aet.artemis.core.security.SecurityUtils; -import de.tum.cit.aet.artemis.programming.domain.build.BuildLogEntry; import de.tum.cit.aet.artemis.programming.domain.build.BuildStatus; /** @@ -397,7 +397,7 @@ private void processBuild(BuildJobQueueItem buildJob) { buildJob.exerciseId(), buildJob.retryCount(), buildJob.priority(), BuildStatus.SUCCESSFUL, buildJob.repositoryInfo(), jobTimingInfo, buildJob.buildConfig(), null); - List buildLogs = buildLogsMap.getBuildLogs(buildJob.id()); + List buildLogs = buildLogsMap.getAndTruncateBuildLogs(buildJob.id()); buildLogsMap.removeBuildLogs(buildJob.id()); ResultQueueItem resultQueueItem = new ResultQueueItem(buildResult, finishedJob, buildLogs, null); @@ -435,7 +435,7 @@ private void processBuild(BuildJobQueueItem buildJob) { job = new BuildJobQueueItem(buildJob, completionDate, status); - List buildLogs = buildLogsMap.getBuildLogs(buildJob.id()); + List buildLogs = buildLogsMap.getAndTruncateBuildLogs(buildJob.id()); buildLogsMap.removeBuildLogs(buildJob.id()); BuildResult failedResult = new BuildResult(buildJob.buildConfig().branch(), buildJob.buildConfig().assignmentCommitHash(), buildJob.buildConfig().testCommitHash(), diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/repository/ConversationMessageRepository.java b/src/main/java/de/tum/cit/aet/artemis/communication/repository/ConversationMessageRepository.java index 21b3dfac0b81..2952c5213432 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/repository/ConversationMessageRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/repository/ConversationMessageRepository.java @@ -137,21 +137,4 @@ default Post findMessagePostByIdElseThrow(Long postId) throws EntityNotFoundExce WHERE p.id = :postId AND answer.author = cp.user """) Set findUsersWhoRepliedInMessage(@Param("postId") Long postId); - - /** - * Finds tags of course-wide messages - * - * @param courseId the course - * @return list of tags - */ - // TODO: unused, delete - @Query(""" - SELECT DISTINCT tag - FROM Post post - LEFT JOIN post.tags tag - LEFT JOIN Channel channel ON channel.id = post.conversation.id - WHERE channel.course.id = :courseId - AND channel.isCourseWide = TRUE - """) - List findPostTagsForCourse(@Param("courseId") Long courseId); } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/repository/CustomPostRepositoryImpl.java b/src/main/java/de/tum/cit/aet/artemis/communication/repository/CustomPostRepositoryImpl.java index cfdd5f71443e..aae587022c93 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/repository/CustomPostRepositoryImpl.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/repository/CustomPostRepositoryImpl.java @@ -1,6 +1,8 @@ package de.tum.cit.aet.artemis.communication.repository; +import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; import jakarta.persistence.EntityManager; @@ -50,6 +52,8 @@ public Page findPostIdsWithSpecification(Specification specification query.setMaxResults(pageable.getPageSize()); List postIds = query.getResultList(); + // removes all duplicates from the answer posts + List uniquePostIds = new ArrayList<>(new LinkedHashSet<>(postIds)); // Count query CriteriaQuery countQuery = builder.createQuery(Long.class); @@ -66,6 +70,6 @@ public Page findPostIdsWithSpecification(Specification specification Long countResult = entityManager.createQuery(countQuery).getSingleResult(); long count = countResult != null ? countResult : 0L; - return new PageImpl<>(postIds, pageable, count); + return new PageImpl<>(uniquePostIds, pageable, count); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/ConversationMessagingService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/ConversationMessagingService.java index 0020deca7faf..a54058431b76 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/ConversationMessagingService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/ConversationMessagingService.java @@ -3,8 +3,6 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import java.time.ZonedDateTime; -import java.util.Comparator; -import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -24,8 +22,6 @@ import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; -import com.google.common.collect.Lists; - import de.tum.cit.aet.artemis.communication.domain.ConversationNotificationRecipientSummary; import de.tum.cit.aet.artemis.communication.domain.CreatedConversationMessage; import de.tum.cit.aet.artemis.communication.domain.DisplayPriority; @@ -49,7 +45,6 @@ import de.tum.cit.aet.artemis.communication.service.conversation.auth.ChannelAuthorizationService; import de.tum.cit.aet.artemis.communication.service.notifications.ConversationNotificationService; import de.tum.cit.aet.artemis.communication.service.notifications.GroupNotificationService; -import de.tum.cit.aet.artemis.communication.service.similarity.PostSimilarityComparisonStrategy; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException; @@ -66,8 +61,6 @@ @Service public class ConversationMessagingService extends PostingService { - private static final int TOP_K_SIMILARITY_RESULTS = 5; - private static final Logger log = LoggerFactory.getLogger(ConversationMessagingService.class); private final ConversationService conversationService; @@ -82,14 +75,11 @@ public class ConversationMessagingService extends PostingService { private final SingleUserNotificationRepository singleUserNotificationRepository; - private final PostSimilarityComparisonStrategy postContentCompareStrategy; - protected ConversationMessagingService(CourseRepository courseRepository, ExerciseRepository exerciseRepository, LectureRepository lectureRepository, ConversationMessageRepository conversationMessageRepository, AuthorizationCheckService authorizationCheckService, WebsocketMessagingService websocketMessagingService, UserRepository userRepository, ConversationService conversationService, ConversationParticipantRepository conversationParticipantRepository, ConversationNotificationService conversationNotificationService, ChannelAuthorizationService channelAuthorizationService, - GroupNotificationService groupNotificationService, SingleUserNotificationRepository singleUserNotificationRepository, - PostSimilarityComparisonStrategy postContentCompareStrategy) { + GroupNotificationService groupNotificationService, SingleUserNotificationRepository singleUserNotificationRepository) { super(courseRepository, userRepository, exerciseRepository, lectureRepository, authorizationCheckService, websocketMessagingService, conversationParticipantRepository); this.conversationService = conversationService; this.conversationMessageRepository = conversationMessageRepository; @@ -97,7 +87,6 @@ protected ConversationMessagingService(CourseRepository courseRepository, Exerci this.channelAuthorizationService = channelAuthorizationService; this.groupNotificationService = groupNotificationService; this.singleUserNotificationRepository = singleUserNotificationRepository; - this.postContentCompareStrategy = postContentCompareStrategy; } /** @@ -433,41 +422,6 @@ private Conversation mayUpdateOrDeleteMessageElseThrow(Post existingMessagePost, } } - /** - * Calculates k similar posts based on the underlying content comparison strategy - * - * @param courseId id of the course in which similar posts are searched for - * @param post post that is to be created and check for similar posts beforehand - * @return list of similar posts - */ - // TODO: unused, remove - public List getSimilarPosts(Long courseId, Post post) { - PostContextFilterDTO postContextFilter = new PostContextFilterDTO(courseId, null, null, null, null, false, false, false, null, null); - List coursePosts = this.getCourseWideMessages(Pageable.unpaged(), postContextFilter, userRepository.getUser(), courseId).stream() - .sorted(Comparator.comparing(coursePost -> postContentCompareStrategy.performSimilarityCheck(post, coursePost))).toList(); - - // sort course posts by calculated similarity scores - setAuthorRoleOfPostings(coursePosts, courseId); - return Lists.reverse(coursePosts).stream().limit(TOP_K_SIMILARITY_RESULTS).toList(); - } - - /** - * Checks course and user validity, - * retrieves all tags for posts in a certain course - * - * @param courseId id of the course the tags belongs to - * @return tags of all posts that belong to the course - */ - // TODO: unused, delete - public List getAllCourseTags(Long courseId) { - final User user = userRepository.getUserWithGroupsAndAuthorities(); - final Course course = courseRepository.findByIdElseThrow(courseId); - - // checks - preCheckUserAndCourseForCommunicationOrMessaging(user, course); - return conversationMessageRepository.findPostTagsForCourse(courseId); - } - @Override public String getEntityName() { return METIS_POST_ENTITY_NAME; 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 d009074927b2..cf5cc2c67cc8 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 @@ -41,11 +41,15 @@ public class ConversationNotificationService { private final SingleUserNotificationRepository singleUserNotificationRepository; + private final SingleUserNotificationService singleUserNotificationService; + public ConversationNotificationService(ConversationNotificationRepository conversationNotificationRepository, - GeneralInstantNotificationService generalInstantNotificationService, SingleUserNotificationRepository singleUserNotificationRepository) { + GeneralInstantNotificationService generalInstantNotificationService, SingleUserNotificationRepository singleUserNotificationRepository, + SingleUserNotificationService singleUserNotificationService) { this.conversationNotificationRepository = conversationNotificationRepository; this.generalInstantNotificationService = generalInstantNotificationService; this.singleUserNotificationRepository = singleUserNotificationRepository; + this.singleUserNotificationService = singleUserNotificationService; } /** @@ -83,7 +87,7 @@ public ConversationNotification createNotification(Post createdMessage, Conversa String[] placeholders = createPlaceholdersNewMessageChannelText(course.getTitle(), createdMessage.getContent(), createdMessage.getCreationDate().toString(), conversationName, createdMessage.getAuthor().getName(), conversationType); ConversationNotification notification = createConversationMessageNotification(course.getId(), createdMessage, notificationType, notificationText, true, placeholders); - save(notification, mentionedUsers, placeholders); + save(notification, mentionedUsers, placeholders, createdMessage); return notification; } @@ -93,11 +97,12 @@ public static String[] createPlaceholdersNewMessageChannelText(String courseTitl return new String[] { courseTitle, messageContent, messageCreationDate, conversationName, authorName, conversationType }; } - private void save(ConversationNotification notification, Set mentionedUsers, String[] placeHolders) { + private void save(ConversationNotification notification, Set mentionedUsers, String[] placeHolders, Post createdMessage) { conversationNotificationRepository.save(notification); - Set mentionedUserNotifications = mentionedUsers.stream().map(mentionedUser -> SingleUserNotificationFactory - .createNotification(notification.getMessage(), NotificationType.CONVERSATION_USER_MENTIONED, notification.getText(), placeHolders, mentionedUser)) + Set mentionedUserNotifications = singleUserNotificationService + .filterAllowedRecipientsInMentionedUsers(mentionedUsers, createdMessage.getConversation()).map(mentionedUser -> SingleUserNotificationFactory + .createNotification(notification.getMessage(), NotificationType.CONVERSATION_USER_MENTIONED, notification.getText(), placeHolders, mentionedUser)) .collect(Collectors.toSet()); singleUserNotificationRepository.saveAll(mentionedUserNotifications); } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/SingleUserNotificationService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/SingleUserNotificationService.java index 4e242f93e0fb..33def5698b9f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/SingleUserNotificationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/SingleUserNotificationService.java @@ -443,23 +443,34 @@ public void notifyInvolvedUsersAboutNewMessageReply(Post post, SingleUserNotific usersInvolved.add(post.getAuthor()); } - mentionedUsers.stream().filter(user -> { - boolean isChannelAndCourseWide = post.getConversation() instanceof Channel channel && channel.getIsCourseWide(); - boolean isChannelVisibleToStudents = !(post.getConversation() instanceof Channel channel) || conversationService.isChannelVisibleToStudents(channel); - boolean isChannelVisibleToMentionedUser = isChannelVisibleToStudents - || authorizationCheckService.isAtLeastTeachingAssistantInCourse(post.getConversation().getCourse(), user); - - // Only send a notification to the mentioned user if... - // (for course-wide channels) ...the course-wide channel is visible - // (for all other cases) ...the user is a member of the conversation - return (isChannelAndCourseWide && isChannelVisibleToMentionedUser) || conversationService.isMember(post.getConversation().getId(), user.getId()); - }).forEach(mentionedUser -> notifyUserAboutNewMessageReply(savedAnswerMessage, notification, mentionedUser, author, CONVERSATION_USER_MENTIONED)); + filterAllowedRecipientsInMentionedUsers(mentionedUsers, post.getConversation()) + .forEach(mentionedUser -> notifyUserAboutNewMessageReply(savedAnswerMessage, notification, mentionedUser, author, CONVERSATION_USER_MENTIONED)); Conversation conv = conversationService.getConversationById(post.getConversation().getId()); usersInvolved.stream().filter(userInvolved -> !mentionedUsers.contains(userInvolved)) .forEach(userInvolved -> notifyUserAboutNewMessageReply(savedAnswerMessage, notification, userInvolved, author, getAnswerMessageNotificationType(conv))); } + /** + * Filters which of the mentioned users are permitted to receive a notification + * + * @param mentionedUsers users mentioned in the answer message + * @param conversation the conversation of the created post/notification, used for filtering + * @return the stream of mentioned users which are permitted to receive the notification for the given conversation + */ + public Stream filterAllowedRecipientsInMentionedUsers(Set mentionedUsers, Conversation conversation) { + return mentionedUsers.stream().filter(user -> { + boolean isChannelAndCourseWide = conversation instanceof Channel channel && channel.getIsCourseWide(); + boolean isChannelVisibleToStudents = !(conversation instanceof Channel channel) || conversationService.isChannelVisibleToStudents(channel); + boolean isChannelVisibleToMentionedUser = isChannelVisibleToStudents || authorizationCheckService.isAtLeastTeachingAssistantInCourse(conversation.getCourse(), user); + + // Only send a notification to the mentioned user if... + // (for course-wide channels) ...the course-wide channel is visible + // (for all other cases) ...the user is a member of the conversation + return (isChannelAndCourseWide && isChannelVisibleToMentionedUser) || conversationService.isMember(conversation.getId(), user.getId()); + }); + } + /** * Saves the given notification in database and sends it to the client via websocket. * Also creates and sends an instant notification. diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/similarity/PostSimilarityComparisonStrategy.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/similarity/PostSimilarityComparisonStrategy.java deleted file mode 100644 index 93ca9de0c9f9..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/similarity/PostSimilarityComparisonStrategy.java +++ /dev/null @@ -1,19 +0,0 @@ -package de.tum.cit.aet.artemis.communication.service.similarity; - -import de.tum.cit.aet.artemis.communication.domain.Post; - -/** - * This interface offers a method that performs a similarity check on two posts that are compared to each other. - * Every strategy that implements this interface has to provide this method in order to be applicable as post similarity comparison strategy, that can be interchanged easily. - */ -public interface PostSimilarityComparisonStrategy { - - /** - * Method implemented by every strategy; compares two posts using any suitable algorithm to determine similarity - * - * @param post1 first post object that is compared against - * @param post2 second post object that is compared against - * @return the calculated similarity score - */ - Double performSimilarityCheck(Post post1, Post post2); -} diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/similarity/TitleJaccardSimilarityCompareStrategy.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/similarity/TitleJaccardSimilarityCompareStrategy.java deleted file mode 100644 index 53e7285cd41b..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/similarity/TitleJaccardSimilarityCompareStrategy.java +++ /dev/null @@ -1,35 +0,0 @@ -package de.tum.cit.aet.artemis.communication.service.similarity; - -import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; - -import org.apache.commons.text.similarity.JaccardSimilarity; -import org.springframework.context.annotation.Primary; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -import de.tum.cit.aet.artemis.communication.domain.Post; - -/** - * Implementation of a PostSimilarityComparisonStrategy to be used when searching for duplicates during post creation. - * Jaccard Similarity is a common proximity measurement used to compute the similarity between two objects, such as two text documents; - * In the context of post comparison, the TitleJaccardSimilarityCompareStrategy determines the similarity between two titles (i.e. document) using the number of terms used in both - * documents. - * We use the JaccardSimilarity implementation provided by the org.apache.commons.text.similarity package. - */ -@Profile(PROFILE_CORE) -@Primary -@Component -public class TitleJaccardSimilarityCompareStrategy implements PostSimilarityComparisonStrategy { - - @Override - public Double performSimilarityCheck(Post post1, Post post2) { - JaccardSimilarity jaccardSimilarity = new JaccardSimilarity(); - Double similarityScore = 0.0; - - // we only compute a similarity score if the title of both posts are defined - if (post1.getTitle() != null && post2.getTitle() != null) { - similarityScore = jaccardSimilarity.apply(post1.getTitle().toLowerCase(), post2.getTitle().toLowerCase()); - } - return similarityScore; - } -} 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 5031332a8862..bfa04d53cc5f 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 @@ -201,34 +201,4 @@ public ResponseEntity updateDisplayPriority(@PathVariable Long courseId, @ Post postWithUpdatedDisplayPriority = conversationMessagingService.changeDisplayPriority(courseId, postId, displayPriority); return ResponseEntity.ok().body(postWithUpdatedDisplayPriority); } - - /** - * POST /courses/{courseId}/messages/similarity-check : trigger a similarity check for post to be created - * - * @param courseId id of the course the post should be published in - * @param post post to create - * @return ResponseEntity with status 200 (OK) - */ - @PostMapping("courses/{courseId}/messages/similarity-check") - @EnforceAtLeastStudent - // TODO: unused, remove - public ResponseEntity> computeSimilarityScoresWitCoursePosts(@PathVariable Long courseId, @RequestBody Post post) { - List similarPosts = conversationMessagingService.getSimilarPosts(courseId, post); - return ResponseEntity.ok().body(similarPosts); - } - - /** - * GET /courses/{courseId}/posts/tags : Get all tags for posts in a certain course - * - * @param courseId id of the course the post belongs to - * @return the ResponseEntity with status 200 (OK) and with body all tags for posts in that course, - * or 400 (Bad Request) if the checks on user or course validity fail - */ - @GetMapping("courses/{courseId}/messages/tags") - // TODO: unused, delete - @EnforceAtLeastStudent - public ResponseEntity> getAllPostTagsForCourse(@PathVariable Long courseId) { - List tags = conversationMessagingService.getAllCourseTags(courseId); - return new ResponseEntity<>(tags, null, HttpStatus.OK); - } } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/WebConfigurer.java b/src/main/java/de/tum/cit/aet/artemis/core/config/WebConfigurer.java index 646fa942e7e0..e79b1de3939c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/config/WebConfigurer.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/WebConfigurer.java @@ -24,6 +24,7 @@ import org.springframework.context.annotation.Profile; import org.springframework.core.env.Environment; import org.springframework.http.MediaType; +import org.springframework.util.CollectionUtils; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; @@ -117,7 +118,7 @@ private String resolvePathPrefix() { public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = jHipsterProperties.getCors(); - if (config.getAllowedOrigins() != null && !config.getAllowedOrigins().isEmpty()) { + if (!CollectionUtils.isEmpty(config.getAllowedOrigins()) || !CollectionUtils.isEmpty(config.getAllowedOriginPatterns())) { log.debug("Registering CORS filter"); source.registerCorsConfiguration("/api/**", config); source.registerCorsConfiguration("/management/**", config); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/cleanup/DataCleanupService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/cleanup/DataCleanupService.java index a89a6f6ef207..2ed93c8d8926 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/cleanup/DataCleanupService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/cleanup/DataCleanupService.java @@ -13,7 +13,6 @@ import de.tum.cit.aet.artemis.assessment.repository.cleanup.FeedbackCleanupRepository; import de.tum.cit.aet.artemis.assessment.repository.cleanup.LongFeedbackTextCleanupRepository; -import de.tum.cit.aet.artemis.assessment.repository.cleanup.ParticipantScoreCleanupRepository; import de.tum.cit.aet.artemis.assessment.repository.cleanup.PlagiarismComparisonCleanupRepository; import de.tum.cit.aet.artemis.assessment.repository.cleanup.RatingCleanupRepository; import de.tum.cit.aet.artemis.assessment.repository.cleanup.ResultCleanupRepository; @@ -49,13 +48,10 @@ public class DataCleanupService { private final TeamScoreCleanupRepository teamScoreCleanupRepository; - private final ParticipantScoreCleanupRepository participantScoreCleanupRepository; - public DataCleanupService(CleanupJobExecutionRepository cleanupJobExecutionRepository, PlagiarismComparisonCleanupRepository plagiarismComparisonCleanupRepository, ResultCleanupRepository resultCleanupRepository, RatingCleanupRepository ratingCleanupRepository, FeedbackCleanupRepository feedbackCleanupRepository, TextBlockCleanupRepository textBlockCleanupRepository, LongFeedbackTextCleanupRepository longFeedbackTextCleanupRepository, - StudentScoreCleanupRepository studentScoreCleanupRepository, TeamScoreCleanupRepository teamScoreCleanupRepository, - ParticipantScoreCleanupRepository participantScoreCleanupRepository) { + StudentScoreCleanupRepository studentScoreCleanupRepository, TeamScoreCleanupRepository teamScoreCleanupRepository) { this.resultCleanupRepository = resultCleanupRepository; this.ratingCleanupRepository = ratingCleanupRepository; this.feedbackCleanupRepository = feedbackCleanupRepository; @@ -65,7 +61,6 @@ public DataCleanupService(CleanupJobExecutionRepository cleanupJobExecutionRepos this.teamScoreCleanupRepository = teamScoreCleanupRepository; this.cleanupJobExecutionRepository = cleanupJobExecutionRepository; this.plagiarismComparisonCleanupRepository = plagiarismComparisonCleanupRepository; - this.participantScoreCleanupRepository = participantScoreCleanupRepository; } // TODO: offer the possibility to delete old submission versions diff --git a/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamLiveEventsService.java b/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamLiveEventsService.java index fa48e0c1a09c..31ee3dbf9e80 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamLiveEventsService.java +++ b/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamLiveEventsService.java @@ -8,7 +8,6 @@ import de.tum.cit.aet.artemis.communication.service.WebsocketMessagingService; import de.tum.cit.aet.artemis.core.domain.User; -import de.tum.cit.aet.artemis.core.repository.UserRepository; import de.tum.cit.aet.artemis.exam.domain.Exam; import de.tum.cit.aet.artemis.exam.domain.StudentExam; import de.tum.cit.aet.artemis.exam.domain.event.ExamAttendanceCheckEvent; @@ -55,14 +54,11 @@ public class ExamLiveEventsService { private final StudentExamRepository studentExamRepository; - private final UserRepository userRepository; - - public ExamLiveEventsService(WebsocketMessagingService websocketMessagingService, ExamLiveEventRepository examLiveEventRepository, StudentExamRepository studentExamRepository, - UserRepository userRepository) { + public ExamLiveEventsService(WebsocketMessagingService websocketMessagingService, ExamLiveEventRepository examLiveEventRepository, + StudentExamRepository studentExamRepository) { this.websocketMessagingService = websocketMessagingService; this.examLiveEventRepository = examLiveEventRepository; this.studentExamRepository = studentExamRepository; - this.userRepository = userRepository; } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/repository/StudentParticipationRepository.java b/src/main/java/de/tum/cit/aet/artemis/exercise/repository/StudentParticipationRepository.java index 1101f94d4708..2c8e4e02eb7c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/repository/StudentParticipationRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/repository/StudentParticipationRepository.java @@ -50,6 +50,9 @@ @Repository public interface StudentParticipationRepository extends ArtemisJpaRepository { + @EntityGraph(type = LOAD, attributePaths = { "team.students" }) + Set findWithTeamInformationByExerciseId(long exerciseId); + Set findByExerciseId(long exerciseId); @Query(""" @@ -59,7 +62,8 @@ public interface StudentParticipationRepository extends ArtemisJpaRepository findByCourseIdWithEagerRatedResults(@Param("courseId") long courseId); @@ -69,8 +73,10 @@ public interface StudentParticipationRepository extends ArtemisJpaRepository findByCourseIdAndStudentIdWithEagerRatedResults(@Param("courseId") long courseId, @Param("studentId") long studentId); @@ -79,7 +85,8 @@ SELECT COUNT(p.id) > 0 FROM StudentParticipation p LEFT JOIN p.team.students ts WHERE p.exercise.course.id = :courseId - AND (p.student.id = :studentId OR ts.id = :studentId) + AND (p.student.id = :studentId + OR ts.id = :studentId) """) boolean existsByCourseIdAndStudentId(@Param("courseId") long courseId, @Param("studentId") long studentId); @@ -91,7 +98,8 @@ SELECT COUNT(p.id) > 0 WHERE p.testRun = FALSE AND p.exercise.exerciseGroup.exam.id = :examId AND r.rated = TRUE - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) """) List findByExamIdWithEagerLegalSubmissionsRatedResults(@Param("examId") long examId); @@ -125,7 +133,8 @@ SELECT COUNT(p.id) > 0 LEFT JOIN FETCH p.submissions s WHERE p.exercise.id = :exerciseId AND p.student.login = :username - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) """) Optional findWithEagerLegalSubmissionsByExerciseIdAndStudentLogin(@Param("exerciseId") long exerciseId, @Param("username") String username); @@ -135,7 +144,8 @@ SELECT COUNT(p.id) > 0 LEFT JOIN FETCH p.submissions s WHERE p.exercise.id = :exerciseId AND p.student.login = :username - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) AND p.testRun = :testRun """) Optional findWithEagerLegalSubmissionsByExerciseIdAndStudentLoginAndTestRun(@Param("exerciseId") long exerciseId, @Param("username") String username, @@ -157,7 +167,8 @@ Optional findWithEagerLegalSubmissionsByExerciseIdAndStude LEFT JOIN FETCH t.students WHERE p.exercise.id = :exerciseId AND p.team.id = :teamId - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) """) Optional findWithEagerLegalSubmissionsAndTeamStudentsByExerciseIdAndTeamId(@Param("exerciseId") long exerciseId, @Param("teamId") long teamId); @@ -175,8 +186,9 @@ SELECT COUNT(p) > 0 FROM StudentParticipation p LEFT JOIN p.team.students u LEFT JOIN p.student s - WHERE p.id = :participationId AND - (s.login = :login OR u.login = :login) + WHERE p.id = :participationId + AND (s.login = :login + OR u.login = :login) """) boolean existsByIdAndParticipatingStudentLogin(@Param("participationId") long participationId, @Param("login") String login); @@ -187,7 +199,8 @@ SELECT COUNT(p) > 0 LEFT JOIN FETCH s.results WHERE p.exercise.id = :exerciseId AND p.testRun = :testRun - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) """) List findByExerciseIdAndTestRunWithEagerLegalSubmissionsResult(@Param("exerciseId") long exerciseId, @Param("testRun") boolean testRun); @@ -235,7 +248,10 @@ Optional findByExerciseIdAndStudentIdAndTestRunWithEagerSu LEFT JOIN FETCH r.assessmentNote WHERE p.exercise.id = :exerciseId AND ( - r.id = (SELECT MAX(p_r.id) FROM p.results p_r) + r.id = ( + SELECT MAX(p_r.id) + FROM p.results p_r + ) OR r.assessmentType <> de.tum.cit.aet.artemis.assessment.domain.AssessmentType.AUTOMATIC OR r IS NULL ) @@ -260,29 +276,16 @@ Optional findByExerciseIdAndStudentIdAndTestRunWithEagerSu LEFT JOIN FETCH t.students WHERE p.exercise.id = :exerciseId AND ( - r.id = (SELECT MAX(p_r.id) FROM p.results p_r) + r.id = ( + SELECT MAX(p_r.id) + FROM p.results p_r + ) OR r.assessmentType <> de.tum.cit.aet.artemis.assessment.domain.AssessmentType.AUTOMATIC OR r IS NULL ) """) Set findByExerciseIdWithLatestAndManualResultsWithTeamInformation(@Param("exerciseId") long exerciseId); - @Query(""" - SELECT DISTINCT p - FROM StudentParticipation p - LEFT JOIN FETCH p.results r - LEFT JOIN FETCH r.submission s - LEFT JOIN FETCH p.submissions - LEFT JOIN FETCH r.assessmentNote - WHERE p.exercise.id = :exerciseId - AND ( - r.id = (SELECT MAX(p_r.id) FROM p.results p_r WHERE p_r.rated = TRUE) - OR r.assessmentType <> de.tum.cit.aet.artemis.assessment.domain.AssessmentType.AUTOMATIC - OR r IS NULL - ) - """) - Set findByExerciseIdWithLatestAndManualRatedResultsAndAssessmentNote(@Param("exerciseId") long exerciseId); - @Query(""" SELECT DISTINCT p FROM StudentParticipation p @@ -292,7 +295,11 @@ Optional findByExerciseIdAndStudentIdAndTestRunWithEagerSu AND p.testRun = :testRun AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) AND r.assessmentType <> de.tum.cit.aet.artemis.assessment.domain.AssessmentType.AUTOMATIC - AND r.id = (SELECT MAX(r2.id) FROM p.results r2 WHERE r2.completionDate IS NOT NULL) + AND r.id = ( + SELECT MAX(r2.id) + FROM p.results r2 + WHERE r2.completionDate IS NOT NULL + ) """) Set findByExerciseIdAndTestRunWithEagerLegalSubmissionsAndLatestResultWithCompletionDate(@Param("exerciseId") long exerciseId, @Param("testRun") boolean testRun); @@ -343,16 +350,14 @@ default List findByExerciseIdWithLatestAutomaticResultAndF LEFT JOIN FETCH f.testCase LEFT JOIN FETCH r.submission s WHERE p.id = :participationId - AND (r.id = ( + AND r.id = ( SELECT MAX(pr.id) FROM p.results pr LEFT JOIN pr.submission prs WHERE pr.assessmentType = de.tum.cit.aet.artemis.assessment.domain.AssessmentType.AUTOMATIC - AND ( - prs.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL - OR prs.type IS NULL - ) - )) + AND (prs.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR prs.type IS NULL) + ) """) Optional findByIdWithLatestAutomaticResultAndFeedbacksAndTestCases(@Param("participationId") long participationId); @@ -366,10 +371,8 @@ SELECT MAX(pr.id) LEFT JOIN FETCH r.submission s WHERE p.exercise.id = :exerciseId AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) - AND ( - r.assessmentType = de.tum.cit.aet.artemis.assessment.domain.AssessmentType.MANUAL - OR r.assessmentType = de.tum.cit.aet.artemis.assessment.domain.AssessmentType.SEMI_AUTOMATIC - ) + AND (r.assessmentType = de.tum.cit.aet.artemis.assessment.domain.AssessmentType.MANUAL + OR r.assessmentType = de.tum.cit.aet.artemis.assessment.domain.AssessmentType.SEMI_AUTOMATIC) """) List findByExerciseIdWithManualResultAndFeedbacksAndTestCases(@Param("exerciseId") long exerciseId); @@ -385,11 +388,10 @@ default List findByExerciseIdWithManualResultAndFeedbacksA LEFT JOIN FETCH f.testCase LEFT JOIN FETCH r.submission s WHERE p.id = :participationId - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) - AND ( - r.assessmentType = de.tum.cit.aet.artemis.assessment.domain.AssessmentType.MANUAL - OR r.assessmentType = de.tum.cit.aet.artemis.assessment.domain.AssessmentType.SEMI_AUTOMATIC - ) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) + AND (r.assessmentType = de.tum.cit.aet.artemis.assessment.domain.AssessmentType.MANUAL + OR r.assessmentType = de.tum.cit.aet.artemis.assessment.domain.AssessmentType.SEMI_AUTOMATIC) """) Optional findByIdWithManualResultAndFeedbacks(@Param("participationId") long participationId); @@ -399,7 +401,8 @@ default List findByExerciseIdWithManualResultAndFeedbacksA LEFT JOIN FETCH p.submissions s WHERE p.exercise.id = :exerciseId AND p.student.id = :studentId - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) """) List findByExerciseIdAndStudentIdWithEagerLegalSubmissions(@Param("exerciseId") long exerciseId, @Param("studentId") long studentId); @@ -427,7 +430,8 @@ default List findByExerciseIdWithManualResultAndFeedbacksA LEFT JOIN FETCH p.submissions s WHERE p.exercise.id = :exerciseId AND p.team.id = :teamId - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) """) List findByExerciseIdAndTeamIdWithEagerLegalSubmissions(@Param("exerciseId") long exerciseId, @Param("teamId") long teamId); @@ -451,8 +455,10 @@ default List findByExerciseIdWithManualResultAndFeedbacksA LEFT JOIN FETCH t.students WHERE p.exercise.id = :exerciseId AND p.team.id = :teamId - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) - AND (rs.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR rs.type IS NULL) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) + AND (rs.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR rs.type IS NULL) """) List findByExerciseIdAndTeamIdWithEagerResultsAndLegalSubmissionsAndTeamStudents(@Param("exerciseId") long exerciseId, @Param("teamId") long teamId); @@ -471,7 +477,8 @@ SELECT MAX(pr.id) LEFT JOIN pr.submission prs WHERE prs.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR prs.type IS NULL - ) OR r.id IS NULL) + ) + OR r.id IS NULL) """) Optional findByExerciseIdAndStudentIdAndTestRunWithLatestResult(@Param("exerciseId") long exerciseId, @Param("studentId") long studentId, @Param("testRun") boolean testRun); @@ -524,7 +531,10 @@ WHERE prs.assessmentType IN ( ) ) AND submission.submitted = TRUE - AND submission.id = (SELECT MAX(s.id) FROM p.submissions s) + AND submission.id = ( + SELECT MAX(s.id) + FROM p.submissions s + ) """) List findByExerciseIdWithLatestSubmissionWithoutManualResultsAndIgnoreTestRunParticipation(@Param("exerciseId") long exerciseId, @Param("correctionRound") long correctionRound); @@ -548,7 +558,10 @@ WHERE prs.assessmentType IN ( de.tum.cit.aet.artemis.assessment.domain.AssessmentType.SEMI_AUTOMATIC ) ) AND s.submitted = TRUE - AND s.id = (SELECT MAX(s.id) FROM p.submissions s) + AND s.id = ( + SELECT MAX(s.id) + FROM p.submissions s + ) """) List findByExerciseIdWithLatestSubmissionWithoutManualResultsWithPassedIndividualDueDateIgnoreTestRuns(@Param("exerciseId") long exerciseId, @Param("now") ZonedDateTime now); @@ -558,7 +571,8 @@ List findByExerciseIdWithLatestSubmissionWithoutManualResu FROM Participation p LEFT JOIN FETCH p.submissions s WHERE p.id = :participationId - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) """) Optional findWithEagerLegalSubmissionsById(@Param("participationId") long participationId); @@ -593,8 +607,10 @@ List findByExerciseIdWithLatestSubmissionWithoutManualResu LEFT JOIN FETCH p.team t LEFT JOIN FETCH t.students WHERE p.id = :participationId - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) - AND (rs.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR rs.type IS NULL) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) + AND (rs.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR rs.type IS NULL) """) Optional findWithEagerLegalSubmissionsResultsFeedbacksById(@Param("participationId") long participationId); @@ -617,10 +633,9 @@ List findByExerciseIdWithLatestSubmissionWithoutManualResu FROM StudentParticipation p JOIN Result r ON r.participation.id = p.id WHERE p.exercise.id = :exerciseId - AND ( - p.student.firstName LIKE %:partialStudentName% - OR p.student.lastName LIKE %:partialStudentName% - ) AND r.completionDate IS NOT NULL + AND (p.student.firstName LIKE %:partialStudentName% + OR p.student.lastName LIKE %:partialStudentName%) + AND r.completionDate IS NOT NULL """) List findIdsByExerciseIdAndStudentName(@Param("exerciseId") long exerciseId, @Param("partialStudentName") String partialStudentName, Pageable pageable); @@ -632,10 +647,9 @@ SELECT COUNT(p) FROM StudentParticipation p JOIN Result r ON r.participation.id = p.id WHERE p.exercise.id = :exerciseId - AND ( - p.student.firstName LIKE %:partialStudentName% - OR p.student.lastName LIKE %:partialStudentName% - ) AND r.completionDate IS NOT NULL + AND (p.student.firstName LIKE %:partialStudentName% + OR p.student.lastName LIKE %:partialStudentName%) + AND r.completionDate IS NOT NULL """) long countByExerciseIdAndStudentName(@Param("exerciseId") long exerciseId, @Param("partialStudentName") String partialStudentName); @@ -666,8 +680,10 @@ default Page findAllWithEagerSubmissionsAndResultsByExerci LEFT JOIN FETCH p.submissions s LEFT JOIN FETCH s.results sr WHERE p.exercise.id = :exerciseId - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) - AND (rs.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR rs.type IS NULL) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) + AND (rs.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR rs.type IS NULL) """) List findAllWithEagerLegalSubmissionsAndEagerResultsByExerciseId(@Param("exerciseId") long exerciseId); @@ -697,12 +713,17 @@ default Page findAllWithEagerSubmissionsAndResultsByExerci LEFT JOIN FETCH p.team WHERE p.exercise.id = :exerciseId AND p.testRun = FALSE - AND s.id = (SELECT MAX(s2.id) - FROM p.submissions s2 - WHERE s2.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s2.type IS NULL) - AND r.id = (SELECT MAX(r2.id) - FROM s.results r2 - WHERE r2.rated = TRUE) + AND s.id = ( + SELECT MAX(s2.id) + FROM p.submissions s2 + WHERE s2.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s2.type IS NULL + ) + AND r.id = ( + SELECT MAX(r2.id) + FROM s.results r2 + WHERE r2.rated = TRUE + ) """) List findAllForPlagiarism(@Param("exerciseId") long exerciseId); @@ -713,7 +734,8 @@ default Page findAllWithEagerSubmissionsAndResultsByExerci LEFT JOIN FETCH s.results r WHERE p.student.id = :studentId AND p.exercise IN :exercises - AND (p.testRun = FALSE OR :includeTestRuns = TRUE) + AND (p.testRun = FALSE + OR :includeTestRuns = TRUE) """) Set findByStudentIdAndIndividualExercisesWithEagerSubmissionsResult(@Param("studentId") long studentId, @Param("exercises") Collection exercises, @Param("includeTestRuns") boolean includeTestRuns); @@ -786,7 +808,8 @@ List findTestRunParticipationsByStudentIdAndIndividualExer LEFT JOIN FETCH t.students teamStudent WHERE teamStudent.id = :studentId AND p.exercise IN :exercises - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) """) Set findByStudentIdAndTeamExercisesWithEagerLegalSubmissionsResult(@Param("studentId") long studentId, @Param("exercises") Collection exercises); @@ -799,7 +822,8 @@ Set findByStudentIdAndTeamExercisesWithEagerLegalSubmissio LEFT JOIN FETCH p.team t WHERE p.exercise.course.id = :courseId AND t.shortName = :teamShortName - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) """) List findAllByCourseIdAndTeamShortNameWithEagerLegalSubmissionsResult(@Param("courseId") long courseId, @Param("teamShortName") String teamShortName); @@ -831,7 +855,8 @@ SELECT p.id, COUNT(s) LEFT JOIN p.submissions s WHERE p.team.shortName = :teamShortName AND p.exercise.course.id = :courseId - AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL OR s.type IS NULL) + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) GROUP BY p.id """) List countLegalSubmissionsPerParticipationByCourseIdAndTeamShortName(@Param("courseId") long courseId, @Param("teamShortName") String teamShortName); @@ -850,7 +875,8 @@ AND EXISTS ( FROM p.submissions s1 WHERE s1.participation.id = p.id AND s1.submitted = TRUE - AND (r.assessor = :assessor OR r.assessor.id IS NULL) + AND (r.assessor = :assessor + OR r.assessor.id IS NULL) ) """) List findAllByParticipationExerciseIdAndResultAssessorAndCorrectionRoundIgnoreTestRuns(@Param("exerciseId") long exerciseId, diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/service/ExerciseDeletionService.java b/src/main/java/de/tum/cit/aet/artemis/exercise/service/ExerciseDeletionService.java index 6739cfc0c714..36b7e99fedd0 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/service/ExerciseDeletionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/service/ExerciseDeletionService.java @@ -17,7 +17,6 @@ import de.tum.cit.aet.artemis.assessment.repository.TutorParticipationRepository; import de.tum.cit.aet.artemis.assessment.service.ExampleSubmissionService; import de.tum.cit.aet.artemis.atlas.domain.competency.CompetencyExerciseLink; -import de.tum.cit.aet.artemis.atlas.repository.CompetencyExerciseLinkRepository; import de.tum.cit.aet.artemis.atlas.service.competency.CompetencyProgressService; import de.tum.cit.aet.artemis.communication.domain.conversation.Channel; import de.tum.cit.aet.artemis.communication.repository.conversation.ChannelRepository; @@ -81,8 +80,6 @@ public class ExerciseDeletionService { private final CompetencyProgressService competencyProgressService; - private final CompetencyExerciseLinkRepository competencyExerciseLinkRepository; - private final Optional irisSettingsService; public ExerciseDeletionService(ExerciseRepository exerciseRepository, ExerciseUnitRepository exerciseUnitRepository, ParticipationService participationService, @@ -90,7 +87,7 @@ public ExerciseDeletionService(ExerciseRepository exerciseRepository, ExerciseUn TutorParticipationRepository tutorParticipationRepository, ExampleSubmissionService exampleSubmissionService, StudentExamRepository studentExamRepository, LectureUnitService lectureUnitService, PlagiarismResultRepository plagiarismResultRepository, TextExerciseService textExerciseService, ChannelRepository channelRepository, ChannelService channelService, CompetencyProgressService competencyProgressService, - CompetencyExerciseLinkRepository competencyExerciseLinkRepository, Optional irisSettingsService) { + Optional irisSettingsService) { this.exerciseRepository = exerciseRepository; this.participationService = participationService; this.programmingExerciseService = programmingExerciseService; @@ -106,7 +103,6 @@ public ExerciseDeletionService(ExerciseRepository exerciseRepository, ExerciseUn this.channelRepository = channelRepository; this.channelService = channelService; this.competencyProgressService = competencyProgressService; - this.competencyExerciseLinkRepository = competencyExerciseLinkRepository; this.irisSettingsService = irisSettingsService; } diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java b/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java index 4d549bb3fe66..3a8c0b6368b9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java @@ -66,7 +66,6 @@ import de.tum.cit.aet.artemis.core.service.messaging.InstanceMessageSendService; import de.tum.cit.aet.artemis.core.util.HeaderUtil; import de.tum.cit.aet.artemis.exercise.domain.Exercise; -import de.tum.cit.aet.artemis.exercise.domain.ExerciseType; import de.tum.cit.aet.artemis.exercise.domain.InitializationState; import de.tum.cit.aet.artemis.exercise.domain.Submission; import de.tum.cit.aet.artemis.exercise.domain.SubmissionType; @@ -595,9 +594,7 @@ public ResponseEntity> updateParticipationDueDates(@P } private Set findParticipationWithLatestResults(Exercise exercise) { - if (exercise.getExerciseType() == ExerciseType.QUIZ) { - return studentParticipationRepository.findByExerciseIdWithLatestAndManualRatedResultsAndAssessmentNote(exercise.getId()); - } + // TODO: we should reduce the amount of data fetched here and sent to the client: double check which data is actually required in the exercise scores page if (exercise.isTeamMode()) { // For team exercises the students need to be eagerly fetched return studentParticipationRepository.findByExerciseIdWithLatestAndManualResultsWithTeamInformation(exercise.getId()); @@ -636,7 +633,12 @@ else if (participation.getSubmissions() != null && !participation.getSubmissions }); } else { - participations = studentParticipationRepository.findByExerciseId(exerciseId); + if (exercise.isTeamMode()) { + participations = studentParticipationRepository.findWithTeamInformationByExerciseId(exerciseId); + } + else { + participations = studentParticipationRepository.findByExerciseId(exerciseId); + } Map submissionCountMap = studentParticipationRepository.countSubmissionsPerParticipationByExerciseIdAsMap(exerciseId); participations.forEach(participation -> participation.setSubmissionCount(submissionCountMap.get(participation.getId()))); diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisExerciseChatSessionResource.java b/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisExerciseChatSessionResource.java index 9e736ce8c358..3f2a0bdb6ab7 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisExerciseChatSessionResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisExerciseChatSessionResource.java @@ -101,7 +101,7 @@ public ResponseEntity getCurrentSessionOrCreateIfNotExi @EnforceAtLeastStudentInExercise public ResponseEntity> getAllSessions(@PathVariable Long exerciseId) { var exercise = exerciseRepository.findByIdElseThrow(exerciseId); - ProgrammingExercise programmingExercise = validateExercise(exercise); + validateExercise(exercise); irisSettingsService.isEnabledForElseThrow(IrisSubSettingsType.CHAT, exercise); var user = userRepository.getUserWithGroupsAndAuthorities(); 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 cc1f57c533fa..c88024f0835b 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 @@ -136,8 +136,8 @@ Optional findWithSubmissionsAndEagerStu @EntityGraph(type = LOAD, attributePaths = { "submissions", "team.students" }) List findWithSubmissionsById(long participationId); - @EntityGraph(type = LOAD, attributePaths = { "submissions" }) - List findWithSubmissionsByExerciseId(long exerciseId); + @EntityGraph(type = LOAD, attributePaths = { "submissions.results" }) + List findWithSubmissionsAndResultsByExerciseId(long exerciseId); @EntityGraph(type = LOAD, attributePaths = { "submissions", "team.students" }) List findWithSubmissionsAndTeamStudentsByExerciseId(long exerciseId); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingSubmissionRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingSubmissionRepository.java index ffe438217b64..90d514ca4b68 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingSubmissionRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingSubmissionRepository.java @@ -50,7 +50,8 @@ default ProgrammingSubmission findFirstByParticipationIdAndCommitHashOrderByIdDe @Query(value = """ SELECT new de.tum.cit.aet.artemis.programming.dto.ProgrammingSubmissionIdAndSubmissionDateDTO(ps.id, ps.submissionDate) FROM ProgrammingSubmission ps - WHERE ps.participation.id = :participationId ORDER BY ps.submissionDate DESC + WHERE ps.participation.id = :participationId + ORDER BY ps.submissionDate DESC """) List findFirstIdByParticipationIdOrderBySubmissionDateDesc(@Param("participationId") long participationId, Pageable pageable); @@ -72,8 +73,8 @@ default Optional findFirstByParticipationIdWithResultsOrd if (result.isEmpty()) { return Optional.empty(); } - long id = result.getFirst().programmingSubmissionId(); - return findProgrammingSubmissionWithResultsById(id); + long submissionId = result.getFirst().programmingSubmissionId(); + return findProgrammingSubmissionWithResultsById(submissionId); } @Query(""" @@ -104,8 +105,7 @@ default Optional findFirstByParticipationIdWithResultsOrd * @return ProgrammingSubmission list (can be empty!) */ default List findGradedByParticipationIdWithResultsOrderBySubmissionDateDesc(long participationId, Pageable pageable) { - List ids = findSubmissionIdsAndDatesByParticipationId(participationId, pageable).stream().map(ProgrammingSubmissionIdAndSubmissionDateDTO::programmingSubmissionId) - .toList(); + var ids = findSubmissionIdsAndDatesByParticipationId(participationId, pageable).stream().map(ProgrammingSubmissionIdAndSubmissionDateDTO::programmingSubmissionId).toList(); if (ids.isEmpty()) { return Collections.emptyList(); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/BuildLogEntryService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/BuildLogEntryService.java index f6143a43561c..039fce2c76bc 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/BuildLogEntryService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/BuildLogEntryService.java @@ -23,6 +23,7 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; +import de.tum.cit.aet.artemis.buildagent.dto.BuildLogDTO; import de.tum.cit.aet.artemis.core.service.ProfileService; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage; @@ -300,14 +301,14 @@ public void deleteBuildLogEntriesForProgrammingSubmission(ProgrammingSubmission * and the build job ID. If the directory structure for the logs does not already exist, it is created. * Each log entry is written to the log file in the format of "time\tlog message". * - * @param buildLogEntries A list of {@link BuildLogEntry} objects containing the build log information to be saved. + * @param buildLogEntries A list of {@link BuildLogDTO} objects containing the build log information to be saved. * @param buildJobId The unique identifier of the build job whose logs are being saved. * @param programmingExercise The programming exercise associated with the build job, used to * retrieve the course and exercise short names. * @throws IllegalStateException If the directory for storing the logs could not be created. * @throws RuntimeException If an I/O error occurs while writing the log file. */ - public void saveBuildLogsToFile(List buildLogEntries, String buildJobId, ProgrammingExercise programmingExercise) { + public void saveBuildLogsToFile(List buildLogEntries, String buildJobId, ProgrammingExercise programmingExercise) { String courseShortName = programmingExercise.getCourseViaExerciseGroupOrCourseMember().getShortName(); String exerciseShortName = programmingExercise.getShortName(); Path exerciseLogsPath = buildLogsPath.resolve(courseShortName).resolve(exerciseShortName); @@ -323,8 +324,8 @@ public void saveBuildLogsToFile(List buildLogEntries, String buil Path logPath = exerciseLogsPath.resolve(buildJobId + ".log"); StringBuilder logsStringBuilder = new StringBuilder(); - for (BuildLogEntry buildLogEntry : buildLogEntries) { - logsStringBuilder.append(buildLogEntry.getTime()).append("\t").append(buildLogEntry.getLog()); + for (BuildLogDTO buildLogEntry : buildLogEntries) { + logsStringBuilder.append(buildLogEntry.time()).append("\t").append(buildLogEntry.log()); } try { diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ParticipationVcsAccessTokenService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ParticipationVcsAccessTokenService.java index db9cebb2eb6c..bbcdc1d5624b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ParticipationVcsAccessTokenService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ParticipationVcsAccessTokenService.java @@ -9,7 +9,9 @@ import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException; +import de.tum.cit.aet.artemis.exercise.domain.Team; import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation; +import de.tum.cit.aet.artemis.exercise.repository.TeamRepository; import de.tum.cit.aet.artemis.programming.domain.ParticipationVCSAccessToken; import de.tum.cit.aet.artemis.programming.repository.ParticipationVCSAccessTokenRepository; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseStudentParticipationRepository; @@ -25,10 +27,13 @@ public class ParticipationVcsAccessTokenService { private final ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository; + private final TeamRepository teamRepository; + public ParticipationVcsAccessTokenService(ParticipationVCSAccessTokenRepository participationVCSAccessTokenRepository, - ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository) { + ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, TeamRepository teamRepository) { this.participationVcsAccessTokenRepository = participationVCSAccessTokenRepository; this.programmingExerciseStudentParticipationRepository = programmingExerciseStudentParticipationRepository; + this.teamRepository = teamRepository; } /** @@ -55,6 +60,7 @@ public ParticipationVCSAccessToken createParticipationVCSAccessToken(User user, */ public ParticipationVCSAccessToken findByUserAndParticipationIdOrElseThrow(User user, long participationId) { var participation = programmingExerciseStudentParticipationRepository.findByIdElseThrow(participationId); + loadTeamStudentsForTeamExercise(participation); if (participation.isOwnedBy(user)) { return participationVcsAccessTokenRepository.findByUserIdAndParticipationIdOrElseThrow(user.getId(), participationId); } @@ -73,6 +79,7 @@ public ParticipationVCSAccessToken findByUserAndParticipationIdOrElseThrow(User public ParticipationVCSAccessToken createVcsAccessTokenForUserAndParticipationIdOrElseThrow(User user, long participationId) { participationVcsAccessTokenRepository.findByUserIdAndParticipationIdAndThrowIfExists(user.getId(), participationId); var participation = programmingExerciseStudentParticipationRepository.findByIdElseThrow(participationId); + loadTeamStudentsForTeamExercise(participation); if (participation.isOwnedBy(user)) { return createParticipationVCSAccessToken(user, participation); } @@ -81,6 +88,19 @@ public ParticipationVCSAccessToken createVcsAccessTokenForUserAndParticipationId } } + /** + * Loads the team students of a participation's team, if it has a team + * + * @param participation the participation which team's students are not loaded yet + */ + private void loadTeamStudentsForTeamExercise(StudentParticipation participation) { + if (participation.getTeam().isPresent()) { + Team team = participation.getTeam().get(); + Team teamWithStudents = teamRepository.findWithStudentsByIdElseThrow(team.getId()); + participation.getTeam().get().setStudents(teamWithStudents.getStudents()); + } + } + /** * Deletes the token connected to a participation * diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingSubmissionService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingSubmissionService.java index 58665d8beae4..9de4485f16b2 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingSubmissionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingSubmissionService.java @@ -6,6 +6,7 @@ import java.time.ZonedDateTime; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -341,14 +342,16 @@ public Optional getLatestPendingSubmission(Long participa * @return a Map of {[participationId]: ProgrammingSubmission | null}. Will contain an entry for every student participation of the exercise and a submission object if a * pending submission exists or null if not. */ - public Map> getLatestPendingSubmissionsForProgrammingExercise(Long programmingExerciseId) { - List participations = programmingExerciseStudentParticipationRepository.findWithSubmissionsByExerciseId(programmingExerciseId); - // TODO: find the latest pending submission directly using Java (the submissions are available now) and not with additional db queries - return participations.stream().collect(Collectors.toMap(Participation::getId, p -> findLatestPendingSubmissionForParticipation(p.getId()))); - } - - private Optional findLatestPendingSubmissionForParticipation(final long participationId) { - return findLatestPendingSubmissionForParticipation(participationId, false); + public Map> getLatestPendingSubmissionsForProgrammingExercise(Long programmingExerciseId) { + var participations = programmingExerciseStudentParticipationRepository.findWithSubmissionsAndResultsByExerciseId(programmingExerciseId); + return participations.stream().collect(Collectors.toMap(Participation::getId, p -> { + var latestSubmission = p.getSubmissions().stream().max(Comparator.comparing(Submission::getSubmissionDate)); + if (latestSubmission.isEmpty() || latestSubmission.get().getLatestResult() != null) { + // This is not an error case, it is very likely that there is no pending submission for a participation. + return Optional.empty(); + } + return latestSubmission; + })); } private Optional findLatestPendingSubmissionForParticipation(final long participationId, final boolean isGraded) { diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIResultProcessingService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIResultProcessingService.java index 71edf64a3fa8..417ee5bdc630 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIResultProcessingService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIResultProcessingService.java @@ -27,6 +27,7 @@ import de.tum.cit.aet.artemis.assessment.domain.Result; import de.tum.cit.aet.artemis.buildagent.dto.BuildAgentInformation; import de.tum.cit.aet.artemis.buildagent.dto.BuildJobQueueItem; +import de.tum.cit.aet.artemis.buildagent.dto.BuildLogDTO; import de.tum.cit.aet.artemis.buildagent.dto.BuildResult; import de.tum.cit.aet.artemis.buildagent.dto.ResultQueueItem; import de.tum.cit.aet.artemis.core.exception.EntityNotFoundException; @@ -37,7 +38,6 @@ import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseParticipation; import de.tum.cit.aet.artemis.programming.domain.RepositoryType; import de.tum.cit.aet.artemis.programming.domain.build.BuildJob; -import de.tum.cit.aet.artemis.programming.domain.build.BuildLogEntry; import de.tum.cit.aet.artemis.programming.domain.build.BuildStatus; import de.tum.cit.aet.artemis.programming.dto.ResultDTO; import de.tum.cit.aet.artemis.programming.exception.BuildTriggerWebsocketError; @@ -133,7 +133,7 @@ public void processResult() { BuildJobQueueItem buildJob = resultQueueItem.buildJobQueueItem(); BuildResult buildResult = resultQueueItem.buildResult(); - List buildLogs = resultQueueItem.buildLogs(); + List buildLogs = resultQueueItem.buildLogs(); Throwable ex = resultQueueItem.exception(); BuildJob savedBuildJob; diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseParticipationResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseParticipationResource.java index be1c99c67be6..2566e1a87fb8 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseParticipationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseParticipationResource.java @@ -37,6 +37,7 @@ import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; import de.tum.cit.aet.artemis.exam.repository.StudentExamRepository; import de.tum.cit.aet.artemis.exam.service.ExamService; +import de.tum.cit.aet.artemis.exercise.domain.Submission; import de.tum.cit.aet.artemis.exercise.domain.participation.Participation; import de.tum.cit.aet.artemis.exercise.repository.ParticipationRepository; import de.tum.cit.aet.artemis.exercise.service.ParticipationAuthorizationCheckService; @@ -230,17 +231,17 @@ public ResponseEntity getLatestPendingSubmission(@PathVar */ @GetMapping("programming-exercises/{exerciseId}/latest-pending-submissions") @EnforceAtLeastTutor - public ResponseEntity>> getLatestPendingSubmissionsByExerciseId(@PathVariable Long exerciseId) { - ProgrammingExercise programmingExercise; - programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationElseThrow(exerciseId); + public ResponseEntity>> getLatestPendingSubmissionsByExerciseId(@PathVariable Long exerciseId) { + ProgrammingExercise programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationElseThrow(exerciseId); if (!authCheckService.isAtLeastTeachingAssistantForExercise(programmingExercise)) { throw new AccessForbiddenException("exercise", exerciseId); } - Map> pendingSubmissions = submissionService.getLatestPendingSubmissionsForProgrammingExercise(exerciseId); + // TODO: use a different data structure than map here + Map> pendingSubmissions = submissionService.getLatestPendingSubmissionsForProgrammingExercise(exerciseId); // Remove unnecessary data to make response smaller (exercise, student of participation). pendingSubmissions = pendingSubmissions.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> { - Optional submissionOpt = entry.getValue(); + Optional submissionOpt = entry.getValue(); // Remove participation, is not needed in the response. submissionOpt.ifPresent(submission -> submission.setParticipation(null)); return submissionOpt; diff --git a/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizExerciseService.java b/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizExerciseService.java index 274718a9b382..955212be3450 100644 --- a/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizExerciseService.java +++ b/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizExerciseService.java @@ -33,7 +33,6 @@ import de.tum.cit.aet.artemis.assessment.domain.Result; import de.tum.cit.aet.artemis.assessment.repository.ResultRepository; -import de.tum.cit.aet.artemis.atlas.repository.CompetencyExerciseLinkRepository; import de.tum.cit.aet.artemis.core.config.Constants; import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.core.dto.SearchResultPageDTO; @@ -87,12 +86,10 @@ public class QuizExerciseService extends QuizService { private final ExerciseService exerciseService; - private final CompetencyExerciseLinkRepository competencyExerciseLinkRepository; - public QuizExerciseService(QuizExerciseRepository quizExerciseRepository, ResultRepository resultRepository, QuizSubmissionRepository quizSubmissionRepository, InstanceMessageSendService instanceMessageSendService, QuizStatisticService quizStatisticService, QuizBatchService quizBatchService, ExerciseSpecificationService exerciseSpecificationService, FileService fileService, DragAndDropMappingRepository dragAndDropMappingRepository, - ShortAnswerMappingRepository shortAnswerMappingRepository, ExerciseService exerciseService, CompetencyExerciseLinkRepository competencyExerciseLinkRepository) { + ShortAnswerMappingRepository shortAnswerMappingRepository, ExerciseService exerciseService) { super(dragAndDropMappingRepository, shortAnswerMappingRepository); this.quizExerciseRepository = quizExerciseRepository; this.resultRepository = resultRepository; @@ -103,7 +100,6 @@ public QuizExerciseService(QuizExerciseRepository quizExerciseRepository, Result this.exerciseSpecificationService = exerciseSpecificationService; this.fileService = fileService; this.exerciseService = exerciseService; - this.competencyExerciseLinkRepository = competencyExerciseLinkRepository; } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizResultService.java b/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizResultService.java index b540fdc5f9bd..4d461ceb1a12 100644 --- a/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizResultService.java +++ b/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizResultService.java @@ -6,6 +6,7 @@ import java.util.Comparator; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import jakarta.validation.constraints.NotNull; @@ -88,9 +89,12 @@ public void evaluateQuizAndUpdateStatistics(@NotNull Long quizExerciseId) { * Evaluate the given quiz exercise by performing the following actions for each participation: * 1. Get the submission for each participation (there should be only one as in exam mode, the submission gets created upfront and will be updated) * - If no submission is found, print a warning and continue as we cannot evaluate that submission - * - If more than one submission is found, select one of them + * - Filter out submissions that are not submitted before the quiz deadline (practice mode) + * - If more than one submission is found, select one with the highest ID * 2. mark submission and participation as evaluated * 3. Create a new result for the selected submission and calculate scores + * - If a rated result already exists, skip the evaluation + * - If no rated result exists, create a new one and evaluate the submission * 4. Save the updated submission & participation and the newly created result *

* After processing all participations, the created results will be returned for further processing @@ -104,6 +108,7 @@ private Set evaluateSubmissions(@NotNull QuizExercise quizExercise) { Set createdResults = new HashSet<>(); List studentParticipations = studentParticipationRepository.findAllWithEagerLegalSubmissionsAndEagerResultsByExerciseId(quizExercise.getId()); submittedAnswerRepository.loadQuizSubmissionsSubmittedAnswers(studentParticipations); + ZonedDateTime quizDeadline = quizExercise.getDueDate(); for (var participation : studentParticipations) { if (participation.isTestRun()) { @@ -122,8 +127,19 @@ private Set evaluateSubmissions(@NotNull QuizExercise quizExercise) { else if (submissions.size() > 1) { log.warn("Found multiple ({}) submissions for participation {} (Participant {}) in quiz {}, taking the one with highest id", submissions.size(), participation.getId(), participation.getParticipant().getName(), quizExercise.getId()); - // Load submission with highest id - quizSubmission = (QuizSubmission) submissions.stream().max(Comparator.comparing(Submission::getId)).get(); + // Filter submissions to only include those submitted before the quiz deadline if the due date is not null, otherwise select the one with the highest ID + Optional validSubmission = submissions.stream() + .filter(submission -> quizExercise.getDueDate() == null + || (submission.getSubmissionDate() != null && !submission.getSubmissionDate().isAfter(quizExercise.getDueDate()))) + .max(Comparator.comparing(Submission::getId)); + if (validSubmission.isPresent()) { + quizSubmission = (QuizSubmission) validSubmission.get(); + } + else { + log.warn("No valid submissions found for participation {} (Participant {}) in quiz {}", participation.getId(), participation.getParticipant().getName(), + quizExercise.getId()); + continue; + } } else { quizSubmission = (QuizSubmission) submissions.iterator().next(); @@ -131,48 +147,41 @@ else if (submissions.size() > 1) { participation.setInitializationState(InitializationState.FINISHED); - boolean resultExisting = false; - // create new result if none is existing - Result result; - if (participation.getResults().isEmpty()) { - result = new Result().participation(participation); + Optional existingRatedResult = participation.getResults().stream().filter(result -> Boolean.TRUE.equals(result.isRated())).findFirst(); + + if (existingRatedResult.isPresent()) { + // A rated result already exists; no need to create a new one + log.debug("A rated result already exists for participation {} (Participant {}), skipping evaluation.", participation.getId(), + participation.getParticipant().getName()); } else { - resultExisting = true; - result = participation.getResults().iterator().next(); - } - // Only create Results once after the first evaluation - if (!resultExisting) { - // delete result from quizSubmission, to be able to set a new one - if (quizSubmission.getLatestResult() != null) { - resultService.deleteResult(quizSubmission.getLatestResult(), true); - } - result.setRated(true); - result.setAssessmentType(AssessmentType.AUTOMATIC); - result.setCompletionDate(ZonedDateTime.now()); + // No rated result exists; create a new one + Result result = new Result().participation(participation).rated(true).assessmentType(AssessmentType.AUTOMATIC).completionDate(ZonedDateTime.now()); - // set submission to calculate scores + // Associate submission with result result.setSubmission(quizSubmission); - // calculate scores and update result and submission accordingly + + // Calculate and update scores quizSubmission.calculateAndUpdateScores(quizExercise.getQuizQuestions()); result.evaluateQuizSubmission(quizExercise); - // remove submission to follow save order for ordered collections + + // Detach submission to maintain proper save order result.setSubmission(null); - // NOTE: we save participation, submission and result here individually so that one exception (e.g. duplicated key) cannot destroy multiple student answers + // Save entities individually submissionRepository.save(quizSubmission); result = resultRepository.save(result); - // add result to participation + // Update participation with new result participation.addResult(result); studentParticipationRepository.save(participation); - // add result to submission + // Re-associate result with submission and save result.setSubmission(quizSubmission); quizSubmission.addResult(result); submissionRepository.save(quizSubmission); - // Add result so that it can be returned (and processed later) + // Add result to the set of created results createdResults.add(result); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/quiz/web/QuizParticipationResource.java b/src/main/java/de/tum/cit/aet/artemis/quiz/web/QuizParticipationResource.java index fc2b4d3b3c94..da732bcdb3ff 100644 --- a/src/main/java/de/tum/cit/aet/artemis/quiz/web/QuizParticipationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/quiz/web/QuizParticipationResource.java @@ -28,7 +28,6 @@ import de.tum.cit.aet.artemis.quiz.domain.QuizExercise; import de.tum.cit.aet.artemis.quiz.repository.QuizExerciseRepository; import de.tum.cit.aet.artemis.quiz.repository.QuizSubmissionRepository; -import de.tum.cit.aet.artemis.quiz.repository.SubmittedAnswerRepository; import de.tum.cit.aet.artemis.quiz.service.QuizBatchService; /** @@ -49,20 +48,16 @@ public class QuizParticipationResource { private final ResultRepository resultRepository; - private final SubmittedAnswerRepository submittedAnswerRepository; - private final QuizSubmissionRepository quizSubmissionRepository; private final QuizBatchService quizBatchService; public QuizParticipationResource(QuizExerciseRepository quizExerciseRepository, ParticipationService participationService, UserRepository userRepository, - ResultRepository resultRepository, SubmittedAnswerRepository submittedAnswerRepository, QuizSubmissionRepository quizSubmissionRepository, - QuizBatchService quizBatchService) { + ResultRepository resultRepository, QuizSubmissionRepository quizSubmissionRepository, QuizBatchService quizBatchService) { this.quizExerciseRepository = quizExerciseRepository; this.participationService = participationService; this.userRepository = userRepository; this.resultRepository = resultRepository; - this.submittedAnswerRepository = submittedAnswerRepository; this.quizSubmissionRepository = quizSubmissionRepository; this.quizBatchService = quizBatchService; } diff --git a/src/main/resources/config/application-buildagent.yml b/src/main/resources/config/application-buildagent.yml index 2872d91575cc..e4c1d3013357 100644 --- a/src/main/resources/config/application-buildagent.yml +++ b/src/main/resources/config/application-buildagent.yml @@ -34,9 +34,12 @@ artemis: cleanup-schedule-minutes: 60 pause-grace-period-seconds: 60 build-timeout-seconds: - min: 10 - default: 120 - max: 240 + min: 10 + default: 120 + max: 240 + build-logs: + max-lines-per-job: 10000 + max-chars-per-line: 1024 git: name: Artemis email: artemis@xcit.tum.de diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml index f2a2506dc1f5..78ae88017f7f 100644 --- a/src/main/resources/config/application.yml +++ b/src/main/resources/config/application.yml @@ -71,20 +71,20 @@ artemis: empty: default: "ubuntu:24.04" python: - default: "ls1tum/artemis-python-docker:latest" + default: "ls1tum/artemis-python-docker:v1.0.0" c: # possible overrides: gcc, fact - default: "ls1tum/artemis-c-docker:latest" - fact: "sharingcodeability/fact:latest" + default: "ls1tum/artemis-c-docker:v1.0.0" + fact: "sharingcodeability/fact:v0.0.5" haskell: default: "ghcr.io/uni-passau-artemis/artemis-haskell:v22.37.0" vhdl: - default: "tizianleonhardt/era-artemis-vhdl:latest" + default: "ghcr.io/ls1intum/artemis-vhdl-docker:v1.0.0" assembler: - default: "tizianleonhardt/era-artemis-assembler:latest" + default: "ghcr.io/ls1intum/artemis-assembler-docker:v1.0.0" swift: # possible overrides: xcode - default: "ls1tum/artemis-swift-swiftlint-docker:latest" + default: "ls1tum/artemis-swift-swiftlint-docker:swift5.9.2" ocaml: default: "ls1tum/artemis-ocaml-docker:v1" rust: @@ -98,7 +98,7 @@ artemis: c_sharp: default: "ghcr.io/ls1intum/artemis-csharp-docker:v1.0.0" typescript: - default: "ghcr.io/ls1intum/artemis-javascript-docker:v1.0.0" + default: "ghcr.io/ls1intum/artemis-javascript-docker:v1.0.0" # The following properties are used to configure the Artemis build agent. # The build agent is responsible for executing the buildJob to test student submissions. diff --git a/src/main/resources/templates/haskell/test/test/Test.hs b/src/main/resources/templates/haskell/test/test/Test.hs index 024ec6008dfd..3f1ec81468e8 100644 --- a/src/main/resources/templates/haskell/test/test/Test.hs +++ b/src/main/resources/templates/haskell/test/test/Test.hs @@ -5,7 +5,9 @@ import qualified Interface as Sub import qualified Solution as Sol import Test.Tasty -import Test.Tasty.Runners.AntXML +import Test.Tasty.Ingredients (composeReporters) +import Test.Tasty.Ingredients.Basic (consoleTestReporter) +import Test.Tasty.Runners.AntXML import Test.SmallCheck.Series as SCS import Test.Tasty.SmallCheck as SC import Test.Tasty.QuickCheck as QC @@ -69,12 +71,12 @@ main = do testRunner $ localOption timeoutOption tests where resultsPath = "test-reports/results.xml" -#ifdef PROD - -- on the server (production mode), run tests with xml output - testRunner = defaultMainWithIngredients [antXMLRunner] +#ifdef PROD + -- on the server (production mode), run tests with additional xml output + testRunner = defaultMainWithIngredients [composeReporters antXMLRunner consoleTestReporter] #else -- locally, run tests with terminal output testRunner = defaultMain -#endif +#endif -- by default, run for 1 second timeoutOption = mkTimeout (1 * 10^6) diff --git a/src/main/webapp/app/detail-overview-list/detail-overview-list.component.html b/src/main/webapp/app/detail-overview-list/detail-overview-list.component.html index 11e5ee4c828f..ba2349c564d3 100644 --- a/src/main/webapp/app/detail-overview-list/detail-overview-list.component.html +++ b/src/main/webapp/app/detail-overview-list/detail-overview-list.component.html @@ -1,7 +1,7 @@ @if (headlines?.length && headlines.length > 1) { } -@for (section of sections; track section) { +@for (section of sections(); track section) {

{{ section.headline | artemisTranslate }}

@for (detail of section.details; track $index) { diff --git a/src/main/webapp/app/detail-overview-list/detail-overview-list.component.ts b/src/main/webapp/app/detail-overview-list/detail-overview-list.component.ts index 25a5a6ca72e6..27eeca162243 100644 --- a/src/main/webapp/app/detail-overview-list/detail-overview-list.component.ts +++ b/src/main/webapp/app/detail-overview-list/detail-overview-list.component.ts @@ -1,7 +1,7 @@ -import { Component, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; +import { Component, OnDestroy, OnInit, ViewEncapsulation, inject, input } from '@angular/core'; import { isEmpty } from 'lodash-es'; import { FeatureToggle } from 'app/shared/feature-toggle/feature-toggle.service'; -import { ButtonSize, TooltipPlacement } from 'app/shared/components/button.component'; +import { ButtonSize } from 'app/shared/components/button.component'; import { IrisSubSettingsType } from 'app/entities/iris/settings/iris-sub-settings.model'; import { ModelingExerciseService } from 'app/exercises/modeling/manage/modeling-exercise.service'; import { AlertService } from 'app/core/util/alert.service'; @@ -50,11 +50,13 @@ export class DetailOverviewListComponent implements OnInit, OnDestroy { protected readonly FeatureToggle = FeatureToggle; protected readonly ButtonSize = ButtonSize; protected readonly ProgrammingExerciseParticipationType = ProgrammingExerciseParticipationType; + protected readonly CHAT = IrisSubSettingsType.CHAT; - readonly CHAT = IrisSubSettingsType.CHAT; + private readonly modelingExerciseService = inject(ModelingExerciseService); + private readonly alertService = inject(AlertService); + private readonly profileService = inject(ProfileService); - @Input() - sections: DetailOverviewSection[]; + sections = input.required(); // headline list for navigation bar headlines: { id: string; translationKey: string }[]; @@ -64,14 +66,8 @@ export class DetailOverviewListComponent implements OnInit, OnDestroy { profileSubscription: Subscription; isLocalVC = false; - constructor( - private modelingExerciseService: ModelingExerciseService, - private alertService: AlertService, - private profileService: ProfileService, - ) {} - ngOnInit() { - this.headlines = this.sections.map((section) => { + this.headlines = this.sections().map((section) => { return { id: section.headline.replaceAll('.', '-'), translationKey: section.headline, @@ -98,6 +94,4 @@ export class DetailOverviewListComponent implements OnInit, OnDestroy { ngOnDestroy() { this.profileSubscription?.unsubscribe(); } - - protected readonly TooltipPlacement = TooltipPlacement; } diff --git a/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/programming-exam-diff/programming-exercise-exam-diff.component.html b/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/programming-exam-diff/programming-exercise-exam-diff.component.html index 7f8623e91b52..cdde46506c49 100644 --- a/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/programming-exam-diff/programming-exercise-exam-diff.component.html +++ b/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/programming-exam-diff/programming-exercise-exam-diff.component.html @@ -1,7 +1,7 @@

{{ exercise.title }} [{{ exercise.maxPoints }} {{ 'artemisApp.examParticipation.points' | artemisTranslate }}] + >[{{ exercise.maxPoints }} {{ 'artemisApp.examParticipation.exercisePoints' | artemisTranslate }}] @if (exercise.includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY) { } diff --git a/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise.component.ts b/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise.component.ts index 01fe874b2afd..0874dea0faf6 100644 --- a/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise.component.ts +++ b/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise.component.ts @@ -1,28 +1,30 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, inject } from '@angular/core'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; -import { ActivatedRoute, Router } from '@angular/router'; -import { TranslateService } from '@ngx-translate/core'; import { filter } from 'rxjs/operators'; import { FileUploadExercise } from 'app/entities/file-upload-exercise.model'; import { FileUploadExerciseService } from './file-upload-exercise.service'; import { ExerciseComponent } from 'app/exercises/shared/exercise/exercise.component'; import { onError } from 'app/shared/util/global.utils'; import { AccountService } from 'app/core/auth/account.service'; -import { CourseManagementService } from 'app/course/manage/course-management.service'; import { SortService } from 'app/shared/service/sort.service'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { AlertService } from 'app/core/util/alert.service'; -import { EventManager } from 'app/core/util/event-manager.service'; import { faBook, faPlus, faSort, faTable, faTrash, faUsers, faWrench } from '@fortawesome/free-solid-svg-icons'; import { faListAlt } from '@fortawesome/free-regular-svg-icons'; import { CourseExerciseService } from 'app/exercises/shared/course-exercises/course-exercise.service'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'jhi-file-upload-exercise', templateUrl: './file-upload-exercise.component.html', }) export class FileUploadExerciseComponent extends ExerciseComponent { + protected exerciseService = inject(ExerciseService); + protected fileUploadExerciseService = inject(FileUploadExerciseService); + private courseExerciseService = inject(CourseExerciseService); + private alertService = inject(AlertService); + private accountService = inject(AccountService); + private sortService = inject(SortService); + @Input() fileUploadExercises: FileUploadExercise[] = []; filteredFileUploadExercises: FileUploadExercise[] = []; @@ -40,23 +42,6 @@ export class FileUploadExerciseComponent extends ExerciseComponent { return this.fileUploadExercises; } - constructor( - public exerciseService: ExerciseService, - public fileUploadExerciseService: FileUploadExerciseService, - private courseExerciseService: CourseExerciseService, - private alertService: AlertService, - private accountService: AccountService, - private modalService: NgbModal, - private router: Router, - private sortService: SortService, - courseService: CourseManagementService, - translateService: TranslateService, - eventManager: EventManager, - route: ActivatedRoute, - ) { - super(courseService, translateService, route, eventManager); - } - protected loadExercises(): void { this.courseExerciseService .findAllFileUploadExercisesForCourse(this.courseId) diff --git a/src/main/webapp/app/exercises/modeling/manage/modeling-exercise.component.ts b/src/main/webapp/app/exercises/modeling/manage/modeling-exercise.component.ts index 74dff3c4b9fc..e1ea83ce136e 100644 --- a/src/main/webapp/app/exercises/modeling/manage/modeling-exercise.component.ts +++ b/src/main/webapp/app/exercises/modeling/manage/modeling-exercise.component.ts @@ -1,18 +1,13 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, inject } from '@angular/core'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { ModelingExercise } from 'app/entities/modeling-exercise.model'; import { ModelingExerciseService } from './modeling-exercise.service'; import { AccountService } from 'app/core/auth/account.service'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { ActivatedRoute, Router } from '@angular/router'; -import { CourseManagementService } from 'app/course/manage/course-management.service'; import { ExerciseComponent } from 'app/exercises/shared/exercise/exercise.component'; -import { TranslateService } from '@ngx-translate/core'; import { onError } from 'app/shared/util/global.utils'; import { SortService } from 'app/shared/service/sort.service'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { AlertService } from 'app/core/util/alert.service'; -import { EventManager } from 'app/core/util/event-manager.service'; import { faBook, faPlus, faSort, faTable, faTimes, faTrash, faUsers, faWrench } from '@fortawesome/free-solid-svg-icons'; import { faListAlt } from '@fortawesome/free-regular-svg-icons'; import { CourseExerciseService } from 'app/exercises/shared/course-exercises/course-exercise.service'; @@ -22,7 +17,14 @@ import { CourseExerciseService } from 'app/exercises/shared/course-exercises/cou templateUrl: './modeling-exercise.component.html', }) export class ModelingExerciseComponent extends ExerciseComponent { - @Input() modelingExercises: ModelingExercise[]; + protected exerciseService = inject(ExerciseService); + protected modelingExerciseService = inject(ModelingExerciseService); + private courseExerciseService = inject(CourseExerciseService); + private alertService = inject(AlertService); + private accountService = inject(AccountService); + private sortService = inject(SortService); + + @Input() modelingExercises: ModelingExercise[] = []; filteredModelingExercises: ModelingExercise[]; // Icons faPlus = faPlus; @@ -39,24 +41,6 @@ export class ModelingExerciseComponent extends ExerciseComponent { return this.modelingExercises; } - constructor( - public exerciseService: ExerciseService, - public modelingExerciseService: ModelingExerciseService, - private courseExerciseService: CourseExerciseService, - private alertService: AlertService, - private accountService: AccountService, - private sortService: SortService, - private modalService: NgbModal, - private router: Router, - courseService: CourseManagementService, - translateService: TranslateService, - eventManager: EventManager, - route: ActivatedRoute, - ) { - super(courseService, translateService, route, eventManager); - this.modelingExercises = []; - } - protected loadExercises(): void { this.courseExerciseService.findAllModelingExercisesForCourse(this.courseId).subscribe({ next: (res: HttpResponse) => { diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts b/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts index 1a1a95462530..baed01da2383 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts @@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { SafeHtml } from '@angular/platform-browser'; import { ProgrammingExerciseBuildConfig } from 'app/entities/programming/programming-exercise-build.config'; -import { Subject, Subscription } from 'rxjs'; +import { Subject, Subscription, of } from 'rxjs'; import { ProgrammingExercise, ProgrammingLanguage } from 'app/entities/programming/programming-exercise.model'; import { ProgrammingExerciseService } from 'app/exercises/programming/manage/services/programming-exercise.service'; import { AlertService, AlertType } from 'app/core/util/alert.service'; @@ -57,6 +57,9 @@ import { IrisSubSettingsType } from 'app/entities/iris/settings/iris-sub-setting import { Detail } from 'app/detail-overview-list/detail.model'; import { Competency } from 'app/entities/competency.model'; import { AeolusService } from 'app/exercises/programming/shared/service/aeolus.service'; +import { mergeMap, tap } from 'rxjs/operators'; +import { ProgrammingExerciseGitDiffReport } from 'app/entities/hestia/programming-exercise-git-diff-report.model'; +import { BuildLogStatisticsDTO } from 'app/entities/programming/build-log-statistics-dto'; @Component({ selector: 'jhi-programming-exercise-detail', @@ -65,15 +68,32 @@ import { AeolusService } from 'app/exercises/programming/shared/service/aeolus.s encapsulation: ViewEncapsulation.None, }) export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { - readonly dayjs = dayjs; - readonly ActionType = ActionType; - readonly ProgrammingExerciseParticipationType = ProgrammingExerciseParticipationType; - readonly FeatureToggle = FeatureToggle; - readonly ProgrammingLanguage = ProgrammingLanguage; - readonly PROGRAMMING = ExerciseType.PROGRAMMING; - readonly ButtonSize = ButtonSize; - readonly AssessmentType = AssessmentType; - readonly documentationType: DocumentationType = 'Programming'; + protected readonly dayjs = dayjs; + protected readonly ActionType = ActionType; + protected readonly ProgrammingExerciseParticipationType = ProgrammingExerciseParticipationType; + protected readonly FeatureToggle = FeatureToggle; + protected readonly ProgrammingLanguage = ProgrammingLanguage; + protected readonly PROGRAMMING = ExerciseType.PROGRAMMING; + protected readonly ButtonSize = ButtonSize; + protected readonly AssessmentType = AssessmentType; + protected readonly documentationType: DocumentationType = 'Programming'; + + protected readonly faUndo = faUndo; + protected readonly faTrash = faTrash; + protected readonly faBook = faBook; + protected readonly faWrench = faWrench; + protected readonly faCheckDouble = faCheckDouble; + protected readonly faTable = faTable; + protected readonly faExclamationTriangle = faExclamationTriangle; + protected readonly faFileSignature = faFileSignature; + protected readonly faListAlt = faListAlt; + protected readonly faChartBar = faChartBar; + protected readonly faLightbulb = faLightbulb; + protected readonly faPencilAlt = faPencilAlt; + protected readonly faUsers = faUsers; + protected readonly faEye = faEye; + protected readonly faUserCheck = faUserCheck; + protected readonly faRobot = faRobot; programmingExercise: ProgrammingExercise; programmingExerciseBuildConfig?: ProgrammingExerciseBuildConfig; @@ -106,10 +126,7 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { private activatedRouteSubscription: Subscription; private templateAndSolutionParticipationSubscription: Subscription; - private profileInfoSubscription: Subscription; private irisSettingsSubscription: Subscription; - private submissionPolicySubscription: Subscription; - private buildLogsSubscription: Subscription; private exerciseStatisticsSubscription: Subscription; private dialogErrorSource = new Subject(); @@ -117,24 +134,6 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { exerciseDetailSections: DetailOverviewSection[]; - // Icons - faUndo = faUndo; - faTrash = faTrash; - faBook = faBook; - faWrench = faWrench; - faCheckDouble = faCheckDouble; - faTable = faTable; - faExclamationTriangle = faExclamationTriangle; - faFileSignature = faFileSignature; - faListAlt = faListAlt; - faChartBar = faChartBar; - faLightbulb = faLightbulb; - faPencilAlt = faPencilAlt; - faUsers = faUsers; - faEye = faEye; - faUserCheck = faUserCheck; - faRobot = faRobot; - constructor( private activatedRoute: ActivatedRoute, private accountService: AccountService, @@ -184,13 +183,15 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { this.templateAndSolutionParticipationSubscription = this.programmingExerciseService .findWithTemplateAndSolutionParticipationAndLatestResults(programmingExercise.id!) - .subscribe((updatedProgrammingExercise) => { - this.programmingExercise = updatedProgrammingExercise.body!; - - this.setLatestCoveredLineRatio(); - this.loadingTemplateParticipationResults = false; - this.loadingSolutionParticipationResults = false; - this.profileInfoSubscription = this.profileService.getProfileInfo().subscribe(async (profileInfo) => { + .pipe( + tap((updatedProgrammingExercise) => { + this.programmingExercise = updatedProgrammingExercise.body!; + this.setLatestCoveredLineRatio(); + this.loadingTemplateParticipationResults = false; + this.loadingSolutionParticipationResults = false; + }), + mergeMap(() => this.profileService.getProfileInfo()), + tap((profileInfo) => { if (profileInfo) { if (this.programmingExercise.projectKey && this.programmingExercise.templateParticipation?.buildPlanId) { this.programmingExercise.templateParticipation.buildPlanUrl = createBuildPlanUrl( @@ -215,38 +216,41 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { if (this.irisEnabled) { this.irisSettingsSubscription = this.irisSettingsService.getCombinedCourseSettings(this.courseId).subscribe((settings) => { this.irisChatEnabled = settings?.irisChatSettings?.enabled ?? false; - this.exerciseDetailSections = this.getExerciseDetails(); }); } } + }), + mergeMap(() => this.programmingExerciseSubmissionPolicyService.getSubmissionPolicyOfProgrammingExercise(exerciseId)), + tap((submissionPolicy) => { + this.programmingExercise.submissionPolicy = submissionPolicy; + }), + mergeMap(() => this.programmingExerciseService.getDiffReport(exerciseId)), + tap((gitDiffReport) => { + this.processGitDiffReport(gitDiffReport, false); + }), + mergeMap(() => + this.programmingExercise.isAtLeastEditor ? this.programmingExerciseService.getBuildLogStatistics(exerciseId!) : of([] as BuildLogStatisticsDTO), + ), + tap((buildLogStatistics) => { + if (this.programmingExercise.isAtLeastEditor) { + this.programmingExercise.buildLogStatistics = buildLogStatistics; + } + }), + ) + .subscribe({ + next: () => { + this.setLatestCoveredLineRatio(); + this.checkAndAlertInconsistencies(); + this.plagiarismCheckSupported = this.programmingLanguageFeatureService.getProgrammingLanguageFeature( + programmingExercise.programmingLanguage, + ).plagiarismCheckSupported; + + /** we make sure to await the results of the subscriptions (switchMap) to only call {@link getExerciseDetails} once */ this.exerciseDetailSections = this.getExerciseDetails(); - }); - - this.submissionPolicySubscription = this.programmingExerciseSubmissionPolicyService - .getSubmissionPolicyOfProgrammingExercise(exerciseId!) - .subscribe((submissionPolicy) => { - this.programmingExercise.submissionPolicy = submissionPolicy; - this.exerciseDetailSections = this.getExerciseDetails(); - }); - - this.loadGitDiffReport(); - - // the build logs endpoint requires at least editor privileges - if (this.programmingExercise.isAtLeastEditor) { - this.buildLogsSubscription = this.programmingExerciseService - .getBuildLogStatistics(exerciseId!) - .subscribe((buildLogStatistics) => (this.programmingExercise.buildLogStatistics = buildLogStatistics)); - this.exerciseDetailSections = this.getExerciseDetails(); - } - - this.setLatestCoveredLineRatio(); - - this.checkAndAlertInconsistencies(); - - this.plagiarismCheckSupported = this.programmingLanguageFeatureService.getProgrammingLanguageFeature( - programmingExercise.programmingLanguage, - ).plagiarismCheckSupported; - this.exerciseDetailSections = this.getExerciseDetails(); + }, + error: (error) => { + this.alertService.error(error.message); + }, }); this.exerciseStatisticsSubscription = this.statisticsService.getExerciseStatistics(exerciseId!).subscribe((statistics: ExerciseManagementStatisticsDto) => { @@ -259,13 +263,17 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { this.dialogErrorSource.unsubscribe(); this.activatedRouteSubscription?.unsubscribe(); this.templateAndSolutionParticipationSubscription?.unsubscribe(); - this.profileInfoSubscription?.unsubscribe(); this.irisSettingsSubscription?.unsubscribe(); - this.submissionPolicySubscription?.unsubscribe(); - this.buildLogsSubscription?.unsubscribe(); this.exerciseStatisticsSubscription?.unsubscribe(); } + /** + * BE CAREFUL WHEN CALLING THIS METHOD!
+ * This method can cause child components to re-render, which can lead to re-initializations resulting + * in unnecessary requests putting load on the server. + * + * When adding a new call to this method, make sure that no duplicated and unnecessary requests are made. + */ getExerciseDetails(): DetailOverviewSection[] { const exercise = this.programmingExercise; exercise.buildConfig = this.programmingExerciseBuildConfig; @@ -780,29 +788,37 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { return link; } - loadGitDiffReport() { - this.programmingExerciseService.getDiffReport(this.programmingExercise.id!).subscribe((gitDiffReport) => { - if ( - gitDiffReport && - (this.programmingExercise.gitDiffReport?.templateRepositoryCommitHash !== gitDiffReport.templateRepositoryCommitHash || - this.programmingExercise.gitDiffReport?.solutionRepositoryCommitHash !== gitDiffReport.solutionRepositoryCommitHash) - ) { - this.programmingExercise.gitDiffReport = gitDiffReport; - gitDiffReport.programmingExercise = this.programmingExercise; - this.addedLineCount = - gitDiffReport.entries - ?.map((entry) => entry.lineCount) - .filter((lineCount) => lineCount) - .map((lineCount) => lineCount!) - .reduce((lineCount1, lineCount2) => lineCount1 + lineCount2, 0) ?? 0; - this.removedLineCount = - gitDiffReport.entries - ?.map((entry) => entry.previousLineCount) - .filter((lineCount) => lineCount) - .map((lineCount) => lineCount!) - .reduce((lineCount1, lineCount2) => lineCount1 + lineCount2, 0) ?? 0; + /** + * + * @param gitDiffReport + * @param updateDetailSections set to false when called from OnInit, as another method will take care to update the + * {@link exerciseDetailSections} to prevent unnecessary renderings and duplicated requests, + * see description of {@link getExerciseDetails} + */ + private processGitDiffReport(gitDiffReport: ProgrammingExerciseGitDiffReport | undefined, updateDetailSections: boolean = true): void { + const isGitDiffReportUpdated = + gitDiffReport && + (this.programmingExercise.gitDiffReport?.templateRepositoryCommitHash !== gitDiffReport.templateRepositoryCommitHash || + this.programmingExercise.gitDiffReport?.solutionRepositoryCommitHash !== gitDiffReport.solutionRepositoryCommitHash); + if (isGitDiffReportUpdated) { + this.programmingExercise.gitDiffReport = gitDiffReport; + gitDiffReport.programmingExercise = this.programmingExercise; + + const calculateLineCount = (entries: { lineCount?: number; previousLineCount?: number }[] = [], key: 'lineCount' | 'previousLineCount') => + entries.map((entry) => entry[key] ?? 0).reduce((sum, count) => sum + count, 0); + + this.addedLineCount = calculateLineCount(gitDiffReport.entries, 'lineCount'); + this.removedLineCount = calculateLineCount(gitDiffReport.entries, 'previousLineCount'); + + if (updateDetailSections) { this.exerciseDetailSections = this.getExerciseDetails(); } + } + } + + loadGitDiffReport() { + this.programmingExerciseService.getDiffReport(this.programmingExercise.id!).subscribe((gitDiffReport) => { + this.processGitDiffReport(gitDiffReport); }); } diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.ts b/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.ts index e1f659c87483..3a33c7b88f83 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.ts @@ -1,23 +1,19 @@ -import { Component, ContentChild, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core'; +import { Component, ContentChild, Input, OnDestroy, OnInit, TemplateRef, inject } from '@angular/core'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { merge } from 'rxjs'; import { ProgrammingExercise } from 'app/entities/programming/programming-exercise.model'; import { ProgrammingExerciseInstructorRepositoryType, ProgrammingExerciseService } from './services/programming-exercise.service'; -import { ActivatedRoute } from '@angular/router'; import { ExerciseComponent } from 'app/exercises/shared/exercise/exercise.component'; -import { TranslateService } from '@ngx-translate/core'; import { ActionType } from 'app/shared/delete-dialog/delete-dialog.model'; import { onError } from 'app/shared/util/global.utils'; import { AccountService } from 'app/core/auth/account.service'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { FeatureToggle } from 'app/shared/feature-toggle/feature-toggle.service'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; -import { CourseManagementService } from 'app/course/manage/course-management.service'; import { SortService } from 'app/shared/service/sort.service'; import { ProgrammingExerciseEditSelectedComponent } from 'app/exercises/programming/manage/programming-exercise-edit-selected.component'; import { ProgrammingExerciseParticipationType } from 'app/entities/programming/programming-exercise-participation.model'; import { AlertService } from 'app/core/util/alert.service'; -import { EventManager } from 'app/core/util/event-manager.service'; import { createBuildPlanUrl } from 'app/exercises/programming/shared/utils/programming-exercise.utils'; import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; import { ConsistencyCheckComponent } from 'app/shared/consistency-check/consistency-check.component'; @@ -46,7 +42,16 @@ import { PROFILE_LOCALCI, PROFILE_LOCALVC, PROFILE_THEIA } from 'app/app.constan templateUrl: './programming-exercise.component.html', }) export class ProgrammingExerciseComponent extends ExerciseComponent implements OnInit, OnDestroy { - @Input() programmingExercises: ProgrammingExercise[]; + protected exerciseService = inject(ExerciseService); + private programmingExerciseService = inject(ProgrammingExerciseService); + private courseExerciseService = inject(CourseExerciseService); + private accountService = inject(AccountService); + private alertService = inject(AlertService); + private modalService = inject(NgbModal); + private sortService = inject(SortService); + private profileService = inject(ProfileService); + + @Input() programmingExercises: ProgrammingExercise[] = []; filteredProgrammingExercises: ProgrammingExercise[]; readonly ActionType = ActionType; FeatureToggle = FeatureToggle; @@ -82,24 +87,6 @@ export class ProgrammingExerciseComponent extends ExerciseComponent implements O return this.programmingExercises; } - constructor( - private programmingExerciseService: ProgrammingExerciseService, - private courseExerciseService: CourseExerciseService, - public exerciseService: ExerciseService, - private accountService: AccountService, - private alertService: AlertService, - private modalService: NgbModal, - private sortService: SortService, - private profileService: ProfileService, - courseService: CourseManagementService, - translateService: TranslateService, - eventManager: EventManager, - route: ActivatedRoute, - ) { - super(courseService, translateService, route, eventManager); - this.programmingExercises = []; - } - ngOnInit(): void { super.ngOnInit(); } diff --git a/src/main/webapp/app/exercises/quiz/manage/quiz-exercise.component.ts b/src/main/webapp/app/exercises/quiz/manage/quiz-exercise.component.ts index 7e3f76e29b65..22ac2e1a7685 100644 --- a/src/main/webapp/app/exercises/quiz/manage/quiz-exercise.component.ts +++ b/src/main/webapp/app/exercises/quiz/manage/quiz-exercise.component.ts @@ -1,18 +1,13 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, inject } from '@angular/core'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { QuizExercise, QuizMode, QuizStatus } from 'app/entities/quiz/quiz-exercise.model'; import { QuizExerciseService } from './quiz-exercise.service'; import { AccountService } from 'app/core/auth/account.service'; -import { ActivatedRoute, Router } from '@angular/router'; -import { CourseManagementService } from 'app/course/manage/course-management.service'; import { ExerciseComponent } from 'app/exercises/shared/exercise/exercise.component'; -import { TranslateService } from '@ngx-translate/core'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { ActionType } from 'app/shared/delete-dialog/delete-dialog.model'; import { SortService } from 'app/shared/service/sort.service'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { AlertService } from 'app/core/util/alert.service'; -import { EventManager } from 'app/core/util/event-manager.service'; import { faSort, faTrash } from '@fortawesome/free-solid-svg-icons'; import { isQuizEditable } from 'app/exercises/quiz/shared/quiz-manage-util.service'; @@ -21,6 +16,12 @@ import { isQuizEditable } from 'app/exercises/quiz/shared/quiz-manage-util.servi templateUrl: './quiz-exercise.component.html', }) export class QuizExerciseComponent extends ExerciseComponent { + protected exerciseService = inject(ExerciseService); + quizExerciseService = inject(QuizExerciseService); + private accountService = inject(AccountService); + private alertService = inject(AlertService); + private sortService = inject(SortService); + readonly ActionType = ActionType; readonly QuizStatus = QuizStatus; readonly QuizMode = QuizMode; @@ -36,22 +37,6 @@ export class QuizExerciseComponent extends ExerciseComponent { return this.quizExercises; } - constructor( - public quizExerciseService: QuizExerciseService, - private accountService: AccountService, - private alertService: AlertService, - private modalService: NgbModal, - private router: Router, - private sortService: SortService, - public exerciseService: ExerciseService, - courseService: CourseManagementService, - translateService: TranslateService, - eventManager: EventManager, - route: ActivatedRoute, - ) { - super(courseService, translateService, route, eventManager); - } - protected loadExercises(): void { this.quizExerciseService.findForCourse(this.courseId).subscribe({ next: (res: HttpResponse) => { diff --git a/src/main/webapp/app/exercises/shared/exercise/exercise.component.ts b/src/main/webapp/app/exercises/shared/exercise/exercise.component.ts index fccf7cdb3269..32d96f26b6f7 100644 --- a/src/main/webapp/app/exercises/shared/exercise/exercise.component.ts +++ b/src/main/webapp/app/exercises/shared/exercise/exercise.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, inject } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable, Subject, Subscription, merge } from 'rxjs'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; @@ -15,6 +15,11 @@ interface DeletionServiceInterface { @Component({ template: '' }) export abstract class ExerciseComponent implements OnInit, OnDestroy { + protected translateService = inject(TranslateService); + protected eventManager = inject(EventManager); + private courseService = inject(CourseManagementService); + private route = inject(ActivatedRoute); + private eventSubscriber: Subscription; @Input() embedded = false; @Input() course: Course; @@ -23,8 +28,8 @@ export abstract class ExerciseComponent implements OnInit, OnDestroy { @Output() filteredExerciseCount = new EventEmitter(); showHeading: boolean; courseId: number; - predicate: string; - reverse: boolean; + predicate: string = 'id'; + reverse: boolean = true; selectedExercises: Exercise[] = []; allChecked = false; @@ -35,16 +40,6 @@ export abstract class ExerciseComponent implements OnInit, OnDestroy { protected abstract get exercises(): Exercise[]; - protected constructor( - private courseService: CourseManagementService, - protected translateService: TranslateService, - private route: ActivatedRoute, - protected eventManager: EventManager, - ) { - this.predicate = 'id'; - this.reverse = true; - } - /** * Fetches an exercise from the server (and if needed the course as well) */ diff --git a/src/main/webapp/app/exercises/shared/participation/participation.component.html b/src/main/webapp/app/exercises/shared/participation/participation.component.html index c3eadf5cd3e1..27f175995eef 100644 --- a/src/main/webapp/app/exercises/shared/participation/participation.component.html +++ b/src/main/webapp/app/exercises/shared/participation/participation.component.html @@ -4,38 +4,36 @@

{{ exercise?.title }} - {{ filteredParticipationsSize }}

- @if (exercise?.type === ExerciseType.PROGRAMMING) { -
- - +
+ + + + @if (exercise.type === ExerciseType.PROGRAMMING && afterDueDate) { - @if (exercise.type === ExerciseType.PROGRAMMING && afterDueDate) { - - } -
- } + } +
@if (exercise?.type !== ExerciseType.QUIZ && exercise?.isAtLeastInstructor) {
diff --git a/src/main/webapp/app/exercises/shared/participation/participation.component.ts b/src/main/webapp/app/exercises/shared/participation/participation.component.ts index 627a33ce8815..c3e018e4b526 100644 --- a/src/main/webapp/app/exercises/shared/participation/participation.component.ts +++ b/src/main/webapp/app/exercises/shared/participation/participation.component.ts @@ -150,7 +150,7 @@ export class ParticipationComponent implements OnInit, OnDestroy { } private loadParticipations(exerciseId: number) { - this.participationService.findAllParticipationsByExercise(exerciseId, true).subscribe((participationsResponse) => { + this.participationService.findAllParticipationsByExercise(exerciseId, false).subscribe((participationsResponse) => { this.participations = participationsResponse.body!; if (this.exercise.type === ExerciseType.PROGRAMMING) { const programmingExercise = this.exercise as ProgrammingExercise; diff --git a/src/main/webapp/app/exercises/text/assess/analytics/text-assesment-analytics.service.ts b/src/main/webapp/app/exercises/text/assess/analytics/text-assesment-analytics.service.ts index 22178ca78f3c..b48b4dbca3ff 100644 --- a/src/main/webapp/app/exercises/text/assess/analytics/text-assesment-analytics.service.ts +++ b/src/main/webapp/app/exercises/text/assess/analytics/text-assesment-analytics.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { TextAssessmentService } from 'app/exercises/text/assess/text-assessment.service'; import { TextAssessmentEvent, TextAssessmentEventType } from 'app/entities/text/text-assesment-event.model'; @@ -13,6 +13,11 @@ import { Location } from '@angular/common'; */ @Injectable({ providedIn: 'root' }) export class TextAssessmentAnalytics { + protected assessmentsService = inject(TextAssessmentService); + protected accountService = inject(AccountService); + private location = inject(Location); + private profileService = inject(ProfileService); + private userId: number; private courseId: number; private textExerciseId: number; @@ -23,12 +28,7 @@ export class TextAssessmentAnalytics { private route: ActivatedRoute; public analyticsEnabled = false; - constructor( - protected assessmentsService: TextAssessmentService, - protected accountService: AccountService, - private profileService: ProfileService, - public location: Location, - ) { + constructor() { // retrieve the analytics enabled status from the profile info and set to current property this.profileService.getProfileInfo().subscribe((profileInfo) => { this.analyticsEnabled = profileInfo.textAssessmentAnalyticsEnabled || false; diff --git a/src/main/webapp/app/exercises/text/assess/text-assessment-area/text-assessment-area.component.ts b/src/main/webapp/app/exercises/text/assess/text-assessment-area/text-assessment-area.component.ts index 200f7b69d95a..2646fccb48ad 100644 --- a/src/main/webapp/app/exercises/text/assess/text-assessment-area/text-assessment-area.component.ts +++ b/src/main/webapp/app/exercises/text/assess/text-assessment-area/text-assessment-area.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, HostListener, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; +import { Component, EventEmitter, HostListener, Input, OnChanges, Output, SimpleChanges, inject } from '@angular/core'; import { TextSubmission } from 'app/entities/text/text-submission.model'; import { TextBlockRef } from 'app/entities/text/text-block-ref.model'; import { StringCountService } from 'app/exercises/text/participate/string-count.service'; @@ -16,6 +16,8 @@ import { GradingCriterion } from 'app/exercises/shared/structured-grading-criter ], }) export class TextAssessmentAreaComponent implements OnChanges { + private stringCountService = inject(StringCountService); + // inputs @Input() submission: TextSubmission; @Input() textBlockRefs: TextBlockRef[]; @@ -32,8 +34,6 @@ export class TextAssessmentAreaComponent implements OnChanges { wordCount = 0; characterCount = 0; - constructor(private stringCountService: StringCountService) {} - /** * Life cycle hook to indicate component change */ diff --git a/src/main/webapp/app/exercises/text/assess/text-assessment-base.component.ts b/src/main/webapp/app/exercises/text/assess/text-assessment-base.component.ts index 8c1f564182d5..a3c60d5352ee 100644 --- a/src/main/webapp/app/exercises/text/assess/text-assessment-base.component.ts +++ b/src/main/webapp/app/exercises/text/assess/text-assessment-base.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, inject } from '@angular/core'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { TextBlockRef } from 'app/entities/text/text-block-ref.model'; import { TextSubmission } from 'app/entities/text/text-submission.model'; @@ -17,6 +17,11 @@ import { getCourseFromExercise } from 'app/entities/exercise.model'; template: '', }) export abstract class TextAssessmentBaseComponent implements OnInit { + protected alertService = inject(AlertService); + protected accountService = inject(AccountService); + protected assessmentsService = inject(TextAssessmentService); + protected structuredGradingCriterionService = inject(StructuredGradingCriterionService); + /* * Base Component for TextSubmissionAssessmentComponent and ExampleTextSubmissionComponent since they share a lot of same functions. */ @@ -29,13 +34,6 @@ export abstract class TextAssessmentBaseComponent implements OnInit { readonly getCourseFromExercise = getCourseFromExercise; - protected constructor( - protected alertService: AlertService, - protected accountService: AccountService, - protected assessmentsService: TextAssessmentService, - protected structuredGradingCriterionService: StructuredGradingCriterionService, - ) {} - async ngOnInit() { // Used to check if the assessor is the current user const identity = await this.accountService.identity(); diff --git a/src/main/webapp/app/exercises/text/assess/text-assessment.service.ts b/src/main/webapp/app/exercises/text/assess/text-assessment.service.ts index 492d620afe9c..6304b95f1625 100644 --- a/src/main/webapp/app/exercises/text/assess/text-assessment.service.ts +++ b/src/main/webapp/app/exercises/text/assess/text-assessment.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map, tap } from 'rxjs/operators'; @@ -22,12 +22,10 @@ type TextAssessmentDTO = { feedbacks: Feedback[]; textBlocks: TextBlock[]; asses providedIn: 'root', }) export class TextAssessmentService { - private readonly RESOURCE_URL = 'api'; + private http = inject(HttpClient); + private accountService = inject(AccountService); - constructor( - private http: HttpClient, - private accountService: AccountService, - ) {} + private readonly RESOURCE_URL = 'api'; /** * Saves the passed feedback items of the assessment. diff --git a/src/main/webapp/app/exercises/text/assess/text-submission-assessment.component.ts b/src/main/webapp/app/exercises/text/assess/text-submission-assessment.component.ts index ed71e52c21d4..1d710d15ac1f 100644 --- a/src/main/webapp/app/exercises/text/assess/text-submission-assessment.component.ts +++ b/src/main/webapp/app/exercises/text/assess/text-submission-assessment.component.ts @@ -1,10 +1,8 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit, inject } from '@angular/core'; import { Location } from '@angular/common'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { ActivatedRoute, Router } from '@angular/router'; -import { AlertService } from 'app/core/util/alert.service'; import dayjs from 'dayjs/esm'; -import { AccountService } from 'app/core/auth/account.service'; import { StudentParticipation } from 'app/entities/participation/student-participation.model'; import { TextSubmission } from 'app/entities/text/text-submission.model'; import { TextExercise } from 'app/entities/text/text-exercise.model'; @@ -16,7 +14,6 @@ import { Feedback, FeedbackType } from 'app/entities/feedback.model'; import { notUndefined, onError } from 'app/shared/util/global.utils'; import { TranslateService } from '@ngx-translate/core'; import { NEW_ASSESSMENT_PATH } from 'app/exercises/text/assess/text-submission-assessment.route'; -import { StructuredGradingCriterionService } from 'app/exercises/shared/structured-grading-criterion/structured-grading-criterion.service'; import { assessmentNavigateBack } from 'app/exercises/shared/navigate-back.util'; import { getLatestSubmissionResult, @@ -45,6 +42,16 @@ import { Subscription } from 'rxjs'; styleUrls: ['./text-submission-assessment.component.scss'], }) export class TextSubmissionAssessmentComponent extends TextAssessmentBaseComponent implements OnInit, OnDestroy { + private activatedRoute = inject(ActivatedRoute); + private router = inject(Router); + private location = inject(Location); + private route = inject(ActivatedRoute); + private complaintService = inject(ComplaintService); + private submissionService = inject(SubmissionService); + private exampleSubmissionService = inject(ExampleSubmissionService); + private athenaService = inject(AthenaService); + private translateService = inject(TranslateService); + /* * The instance of this component is REUSED for multiple assessments if using the "Assess Next" button! * All properties must be initialized with a default value (or null) in the resetComponent() method. @@ -66,7 +73,7 @@ export class TextSubmissionAssessmentComponent extends TextAssessmentBaseCompone assessmentsAreValid: boolean; noNewSubmissions: boolean; hasAssessmentDueDatePassed: boolean; - correctionRound: number; + correctionRound: number = 0; resultId: number; loadingInitialSubmission = true; highlightDifferences = false; @@ -99,24 +106,9 @@ export class TextSubmissionAssessmentComponent extends TextAssessmentBaseCompone // Icons farListAlt = faListAlt; - constructor( - private activatedRoute: ActivatedRoute, - private router: Router, - private location: Location, - private route: ActivatedRoute, - private complaintService: ComplaintService, - private submissionService: SubmissionService, - private exampleSubmissionService: ExampleSubmissionService, - private athenaService: AthenaService, - alertService: AlertService, - accountService: AccountService, - assessmentsService: TextAssessmentService, - structuredGradingCriterionService: StructuredGradingCriterionService, - translateService: TranslateService, - ) { - super(alertService, accountService, assessmentsService, structuredGradingCriterionService); - translateService.get('artemisApp.textAssessment.confirmCancel').subscribe((text) => (this.cancelConfirmationText = text)); - this.correctionRound = 0; + constructor() { + super(); + this.translateService.get('artemisApp.textAssessment.confirmCancel').subscribe((text) => (this.cancelConfirmationText = text)); this.resetComponent(); } diff --git a/src/main/webapp/app/exercises/text/assess/text-submission-assessment.route.ts b/src/main/webapp/app/exercises/text/assess/text-submission-assessment.route.ts index 83e63bcd21b1..aa8fb3f15542 100644 --- a/src/main/webapp/app/exercises/text/assess/text-submission-assessment.route.ts +++ b/src/main/webapp/app/exercises/text/assess/text-submission-assessment.route.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { ActivatedRouteSnapshot, Resolve, Routes } from '@angular/router'; import { TextSubmission } from 'app/entities/text/text-submission.model'; import { of } from 'rxjs'; @@ -12,7 +12,7 @@ import { catchError, map } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class NewStudentParticipationResolver implements Resolve { - constructor(private textSubmissionService: TextSubmissionService) {} + private textSubmissionService = inject(TextSubmissionService); /** * Resolves the needed StudentParticipations for the TextSubmissionAssessmentComponent using the TextAssessmentService. @@ -33,7 +33,7 @@ export class NewStudentParticipationResolver implements Resolve { - constructor(private textAssessmentService: TextAssessmentService) {} + private textAssessmentService = inject(TextAssessmentService); /** * Resolves the needed StudentParticipations for the TextSubmissionAssessmentComponent using the TextAssessmentService. diff --git a/src/main/webapp/app/exercises/text/assess/textblock-assessment-card/textblock-assessment-card.component.ts b/src/main/webapp/app/exercises/text/assess/textblock-assessment-card/textblock-assessment-card.component.ts index 2a34e2b47136..402d82924caa 100644 --- a/src/main/webapp/app/exercises/text/assess/textblock-assessment-card/textblock-assessment-card.component.ts +++ b/src/main/webapp/app/exercises/text/assess/textblock-assessment-card/textblock-assessment-card.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; +import { Component, EventEmitter, Input, Output, ViewChild, inject } from '@angular/core'; import { TextBlockRef } from 'app/entities/text/text-block-ref.model'; import { TextblockFeedbackEditorComponent } from 'app/exercises/text/assess/textblock-feedback-editor/textblock-feedback-editor.component'; import { StructuredGradingCriterionService } from 'app/exercises/shared/structured-grading-criterion/structured-grading-criterion.service'; @@ -17,6 +17,10 @@ type OptionalTextBlockRef = TextBlockRef | undefined; styleUrls: ['./textblock-assessment-card.component.scss'], }) export class TextblockAssessmentCardComponent { + protected route = inject(ActivatedRoute); + private structuredGradingCriterionService = inject(StructuredGradingCriterionService); + textAssessmentAnalytics = inject(TextAssessmentAnalytics); + @Input() textBlockRef: TextBlockRef; @Input() selected = false; @Input() readOnly: boolean; @@ -29,12 +33,8 @@ export class TextblockAssessmentCardComponent { @Output() didDelete = new EventEmitter(); @ViewChild(TextblockFeedbackEditorComponent) feedbackEditor: TextblockFeedbackEditorComponent; - constructor( - public structuredGradingCriterionService: StructuredGradingCriterionService, - public textAssessmentAnalytics: TextAssessmentAnalytics, - protected route: ActivatedRoute, - ) { - textAssessmentAnalytics.setComponentRoute(route); + constructor() { + this.textAssessmentAnalytics.setComponentRoute(this.route); } /** diff --git a/src/main/webapp/app/exercises/text/assess/textblock-feedback-editor/textblock-feedback-editor.component.ts b/src/main/webapp/app/exercises/text/assess/textblock-feedback-editor/textblock-feedback-editor.component.ts index 0cae1c87a35b..d52448b4164c 100644 --- a/src/main/webapp/app/exercises/text/assess/textblock-feedback-editor/textblock-feedback-editor.component.ts +++ b/src/main/webapp/app/exercises/text/assess/textblock-feedback-editor/textblock-feedback-editor.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, ElementRef, EventEmitter, HostBinding, Input, Output, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, ElementRef, EventEmitter, HostBinding, Input, Output, ViewChild, inject } from '@angular/core'; import { TextBlock } from 'app/entities/text/text-block.model'; import { Feedback, FeedbackType } from 'app/entities/feedback.model'; import { ConfirmIconComponent } from 'app/shared/confirm-icon/confirm-icon.component'; @@ -16,6 +16,11 @@ import { GradingCriterion } from 'app/exercises/shared/structured-grading-criter styleUrls: ['./textblock-feedback-editor.component.scss'], }) export class TextblockFeedbackEditorComponent implements AfterViewInit { + protected route = inject(ActivatedRoute); + protected modalService = inject(NgbModal); + structuredGradingCriterionService = inject(StructuredGradingCriterionService); + textAssessmentAnalytics = inject(TextAssessmentAnalytics); + readonly FeedbackType = FeedbackType; @Input() textBlock: TextBlock = new TextBlock(); @@ -55,13 +60,8 @@ export class TextblockFeedbackEditorComponent implements AfterViewInit { faTrash = faTrash; faAngleRight = faAngleRight; - constructor( - public structuredGradingCriterionService: StructuredGradingCriterionService, - protected modalService: NgbModal, - protected route: ActivatedRoute, - public textAssessmentAnalytics: TextAssessmentAnalytics, - ) { - textAssessmentAnalytics.setComponentRoute(route); + constructor() { + this.textAssessmentAnalytics.setComponentRoute(this.route); } /** diff --git a/src/main/webapp/app/exercises/text/manage/example-text-submission/example-text-submission.component.ts b/src/main/webapp/app/exercises/text/manage/example-text-submission/example-text-submission.component.ts index 1640da7b1978..9592fcd4c481 100644 --- a/src/main/webapp/app/exercises/text/manage/example-text-submission/example-text-submission.component.ts +++ b/src/main/webapp/app/exercises/text/manage/example-text-submission/example-text-submission.component.ts @@ -1,11 +1,9 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, inject } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { AlertService } from 'app/core/util/alert.service'; import { HttpResponse } from '@angular/common/http'; import { EntityResponseType, ExampleSubmissionService } from 'app/exercises/shared/example-submission/example-submission.service'; import { TextAssessmentService } from 'app/exercises/text/assess/text-assessment.service'; import { TutorParticipationService } from 'app/exercises/shared/dashboards/tutor/tutor-participation.service'; -import { AccountService } from 'app/core/auth/account.service'; import { GuidedTourService } from 'app/guided-tour/guided-tour.service'; import { tutorAssessmentTour } from 'app/guided-tour/tours/tutor-assessment-tour'; import { ExampleSubmission, ExampleSubmissionMode } from 'app/entities/example-submission.model'; @@ -16,7 +14,6 @@ import { TextSubmission } from 'app/entities/text/text-submission.model'; import { Result } from 'app/entities/result.model'; import { setLatestSubmissionResult } from 'app/entities/submission.model'; import { TextAssessmentBaseComponent } from 'app/exercises/text/assess/text-assessment-base.component'; -import { StructuredGradingCriterionService } from 'app/exercises/shared/structured-grading-criterion/structured-grading-criterion.service'; import { notUndefined } from 'app/shared/util/global.utils'; import { AssessButtonStates, Context, State, SubmissionButtonStates, UIStates } from 'app/exercises/text/manage/example-text-submission/example-text-submission-state.model'; import { filter, mergeMap, switchMap, tap } from 'rxjs/operators'; @@ -35,6 +32,14 @@ type ExampleSubmissionResponseType = EntityResponseType; styleUrls: ['./example-text-submission.component.scss'], }) export class ExampleTextSubmissionComponent extends TextAssessmentBaseComponent implements OnInit, Context, FeedbackMarker { + private route = inject(ActivatedRoute); + private router = inject(Router); + private exampleSubmissionService = inject(ExampleSubmissionService); + private tutorParticipationService = inject(TutorParticipationService); + private guidedTourService = inject(GuidedTourService); + private navigationUtilService = inject(ArtemisNavigationUtilService); + private exerciseService = inject(ExerciseService); + isNewSubmission: boolean; areNewAssessments = true; @@ -62,20 +67,8 @@ export class ExampleTextSubmissionComponent extends TextAssessmentBaseComponent faEdit = faEdit; farListAlt = faListAlt; - constructor( - private exampleSubmissionService: ExampleSubmissionService, - private tutorParticipationService: TutorParticipationService, - private route: ActivatedRoute, - private router: Router, - private guidedTourService: GuidedTourService, - private navigationUtilService: ArtemisNavigationUtilService, - private exerciseService: ExerciseService, - alertService: AlertService, - accountService: AccountService, - assessmentsService: TextAssessmentService, - structuredGradingCriterionService: StructuredGradingCriterionService, - ) { - super(alertService, accountService, assessmentsService, structuredGradingCriterionService); + constructor() { + super(); this.textBlockRefs = []; this.unusedTextBlockRefs = []; this.submission = new TextSubmission(); diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-detail.component.ts b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-detail.component.ts index e758013e44ee..92c75b1db510 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-detail.component.ts +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-detail.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit, inject } from '@angular/core'; import { SafeHtml } from '@angular/platform-browser'; import { ActivatedRoute } from '@angular/router'; import { HttpResponse } from '@angular/common/http'; @@ -29,6 +29,12 @@ import { templateUrl: './text-exercise-detail.component.html', }) export class TextExerciseDetailComponent implements OnInit, OnDestroy { + private route = inject(ActivatedRoute); + private eventManager = inject(EventManager); + private artemisMarkdownService = inject(ArtemisMarkdownService); + private textExerciseService = inject(TextExerciseService); + private statisticsService = inject(StatisticsService); + readonly documentationType: DocumentationType = 'Text'; readonly AssessmentType = AssessmentType; @@ -48,14 +54,6 @@ export class TextExerciseDetailComponent implements OnInit, OnDestroy { private subscription: Subscription; private eventSubscriber: Subscription; - constructor( - private eventManager: EventManager, - private textExerciseService: TextExerciseService, - private route: ActivatedRoute, - private artemisMarkdown: ArtemisMarkdownService, - private statisticsService: StatisticsService, - ) {} - /** * Loads the text exercise and subscribes to changes of it on component initialization. */ @@ -78,9 +76,9 @@ export class TextExerciseDetailComponent implements OnInit, OnDestroy { this.isExamExercise = !!this.textExercise.exerciseGroup; this.course = this.isExamExercise ? this.textExercise.exerciseGroup?.exam?.course : this.textExercise.course; - this.formattedGradingInstructions = this.artemisMarkdown.safeHtmlForMarkdown(this.textExercise.gradingInstructions); - this.formattedProblemStatement = this.artemisMarkdown.safeHtmlForMarkdown(this.textExercise.problemStatement); - this.formattedExampleSolution = this.artemisMarkdown.safeHtmlForMarkdown(this.textExercise.exampleSolution); + this.formattedGradingInstructions = this.artemisMarkdownService.safeHtmlForMarkdown(this.textExercise.gradingInstructions); + this.formattedProblemStatement = this.artemisMarkdownService.safeHtmlForMarkdown(this.textExercise.problemStatement); + this.formattedExampleSolution = this.artemisMarkdownService.safeHtmlForMarkdown(this.textExercise.exampleSolution); this.detailOverviewSections = this.getExerciseDetailSections(); }); this.statisticsService.getExerciseStatistics(exerciseId).subscribe((statistics: ExerciseManagementStatisticsDto) => { diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-paging.service.ts b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-paging.service.ts index ed167d578dd0..6f5538b09ed1 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-paging.service.ts +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-paging.service.ts @@ -1,5 +1,5 @@ import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { TextExercise } from 'app/entities/text/text-exercise.model'; import { ExercisePagingService } from 'app/exercises/shared/manage/exercise-paging.service'; @@ -7,7 +7,8 @@ import { ExercisePagingService } from 'app/exercises/shared/manage/exercise-pagi export class TextExercisePagingService extends ExercisePagingService { private static readonly RESOURCE_URL = 'api/text-exercises'; - constructor(http: HttpClient) { + constructor() { + const http = inject(HttpClient); super(http, TextExercisePagingService.RESOURCE_URL); } } diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-row-buttons.component.ts b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-row-buttons.component.ts index 00e2973a5cc6..a1aab34bf04f 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-row-buttons.component.ts +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-row-buttons.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, inject } from '@angular/core'; import { HttpErrorResponse } from '@angular/common/http'; import { Subject } from 'rxjs'; import { TextExerciseService } from 'app/exercises/text/manage/text-exercise/text-exercise.service'; @@ -12,6 +12,9 @@ import { faListAlt } from '@fortawesome/free-regular-svg-icons'; templateUrl: './text-exercise-row-buttons.component.html', }) export class TextExerciseRowButtonsComponent { + private eventManager = inject(EventManager); + private textExerciseService = inject(TextExerciseService); + @Input() courseId: number; @Input() exercise: TextExercise; private dialogErrorSource = new Subject(); @@ -25,11 +28,6 @@ export class TextExerciseRowButtonsComponent { faTable = faTable; farListAlt = faListAlt; - constructor( - private textExerciseService: TextExerciseService, - private eventManager: EventManager, - ) {} - deleteExercise() { this.textExerciseService.delete(this.exercise.id!).subscribe({ next: () => { diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.ts b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.ts index e6abb283f200..18a5e8131205 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.ts +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit, ViewChild, inject } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { HttpErrorResponse } from '@angular/common/http'; import { TextExercise } from 'app/entities/text/text-exercise.model'; @@ -37,6 +37,18 @@ import { FormulaAction } from 'app/shared/monaco-editor/model/actions/formula.ac changeDetection: ChangeDetectionStrategy.OnPush, }) export class TextExerciseUpdateComponent implements OnInit, OnDestroy, AfterViewInit { + private activatedRoute = inject(ActivatedRoute); + private alertService = inject(AlertService); + private textExerciseService = inject(TextExerciseService); + private modalService = inject(NgbModal); + private popupService = inject(ExerciseUpdateWarningService); + private exerciseService = inject(ExerciseService); + private exerciseGroupService = inject(ExerciseGroupService); + private courseService = inject(CourseManagementService); + private eventManager = inject(EventManager); + private navigationUtilService = inject(ArtemisNavigationUtilService); + private athenaService = inject(AthenaService); + readonly IncludedInOverallScore = IncludedInOverallScore; readonly documentationType: DocumentationType = 'Text'; @@ -78,20 +90,6 @@ export class TextExerciseUpdateComponent implements OnInit, OnDestroy, AfterView plagiarismSubscription?: Subscription; teamSubscription?: Subscription; - constructor( - private alertService: AlertService, - private textExerciseService: TextExerciseService, - private modalService: NgbModal, - private popupService: ExerciseUpdateWarningService, - private exerciseService: ExerciseService, - private exerciseGroupService: ExerciseGroupService, - private courseService: CourseManagementService, - private eventManager: EventManager, - private activatedRoute: ActivatedRoute, - private navigationUtilService: ArtemisNavigationUtilService, - private athenaService: AthenaService, - ) {} - get editType(): EditType { if (this.isImport) { return EditType.IMPORT; diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.component.ts b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.component.ts index ac46f89650d7..1ab47a466b49 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.component.ts +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.component.ts @@ -1,19 +1,16 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, inject } from '@angular/core'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { ExerciseType } from 'app/entities/exercise.model'; import { TextExercise } from 'app/entities/text/text-exercise.model'; import { TextExerciseService } from './text-exercise.service'; -import { CourseManagementService } from 'app/course/manage/course-management.service'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { ActivatedRoute, Router } from '@angular/router'; +import { Router } from '@angular/router'; import { ExerciseComponent } from 'app/exercises/shared/exercise/exercise.component'; -import { TranslateService } from '@ngx-translate/core'; import { onError } from 'app/shared/util/global.utils'; import { AccountService } from 'app/core/auth/account.service'; import { SortService } from 'app/shared/service/sort.service'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { AlertService } from 'app/core/util/alert.service'; -import { EventManager } from 'app/core/util/event-manager.service'; import { faPlus, faSort, faTrash } from '@fortawesome/free-solid-svg-icons'; import { CourseExerciseService } from 'app/exercises/shared/course-exercises/course-exercise.service'; import { ExerciseImportWrapperComponent } from 'app/exercises/shared/import/exercise-import-wrapper/exercise-import-wrapper.component'; @@ -23,8 +20,17 @@ import { ExerciseImportWrapperComponent } from 'app/exercises/shared/import/exer templateUrl: './text-exercise.component.html', }) export class TextExerciseComponent extends ExerciseComponent { - @Input() textExercises: TextExercise[]; - filteredTextExercises: TextExercise[]; + protected exerciseService = inject(ExerciseService); + protected textExerciseService = inject(TextExerciseService); + private router = inject(Router); + private courseExerciseService = inject(CourseExerciseService); + private modalService = inject(NgbModal); + private alertService = inject(AlertService); + private sortService = inject(SortService); + private accountService = inject(AccountService); + + @Input() textExercises: TextExercise[] = []; + filteredTextExercises: TextExercise[] = []; // Icons faSort = faSort; @@ -35,25 +41,6 @@ export class TextExerciseComponent extends ExerciseComponent { return this.textExercises; } - constructor( - public exerciseService: ExerciseService, - public textExerciseService: TextExerciseService, - private courseExerciseService: CourseExerciseService, - private modalService: NgbModal, - private router: Router, - courseService: CourseManagementService, - translateService: TranslateService, - private alertService: AlertService, - private sortService: SortService, - eventManager: EventManager, - route: ActivatedRoute, - private accountService: AccountService, - ) { - super(courseService, translateService, route, eventManager); - this.textExercises = []; - this.filteredTextExercises = []; - } - protected loadExercises(): void { this.courseExerciseService.findAllTextExercisesForCourse(this.courseId).subscribe({ next: (res: HttpResponse) => { diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.route.ts b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.route.ts index 30d4763745fc..d5431d66cc5f 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.route.ts +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.route.ts @@ -3,7 +3,7 @@ import { UserRouteAccessService } from 'app/core/auth/user-route-access-service' import { TextExerciseDetailComponent } from './text-exercise-detail.component'; import { TextExerciseUpdateComponent } from './text-exercise-update.component'; import { TextExercise } from 'app/entities/text/text-exercise.model'; -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { TextExerciseService } from 'app/exercises/text/manage/text-exercise/text-exercise.service'; import { CourseManagementService } from 'app/course/manage/course-management.service'; import { of } from 'rxjs'; @@ -19,11 +19,9 @@ import { ExampleSubmissionsComponent } from 'app/exercises/shared/example-submis @Injectable({ providedIn: 'root' }) export class TextExerciseResolver implements Resolve { - constructor( - private textExerciseService: TextExerciseService, - private courseService: CourseManagementService, - private exerciseGroupService: ExerciseGroupService, - ) {} + private textExerciseService = inject(TextExerciseService); + private courseService = inject(CourseManagementService); + private exerciseGroupService = inject(ExerciseGroupService); /** * Resolves the route and initializes text exercise diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.service.ts b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.service.ts index 994f1ac75eeb..5c6b9275b3a1 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.service.ts +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { HttpClient, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -16,12 +16,10 @@ export type EntityArrayResponseType = HttpResponse; @Injectable({ providedIn: 'root' }) export class TextExerciseService implements ExerciseServicable { - private resourceUrl = 'api/text-exercises'; + private http = inject(HttpClient); + private exerciseService = inject(ExerciseService); - constructor( - private http: HttpClient, - private exerciseService: ExerciseService, - ) {} + private resourceUrl = 'api/text-exercises'; /** * Store a new text exercise on the server. diff --git a/src/main/webapp/app/exercises/text/manage/tutor-effort/tutor-effort-statistics.component.ts b/src/main/webapp/app/exercises/text/manage/tutor-effort/tutor-effort-statistics.component.ts index e6c54d0ce175..45d7cb002e0b 100644 --- a/src/main/webapp/app/exercises/text/manage/tutor-effort/tutor-effort-statistics.component.ts +++ b/src/main/webapp/app/exercises/text/manage/tutor-effort/tutor-effort-statistics.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, inject } from '@angular/core'; import { TutorEffort } from 'app/entities/tutor-effort.model'; import { TextExerciseService } from 'app/exercises/text/manage/text-exercise/text-exercise.service'; import { ActivatedRoute, Router } from '@angular/router'; @@ -21,6 +21,12 @@ interface TutorEffortRange { styleUrls: ['./tutor-effort-statistics.component.scss'], }) export class TutorEffortStatisticsComponent extends PlagiarismAndTutorEffortDirective implements OnInit { + private route = inject(ActivatedRoute); + private router = inject(Router); + private textExerciseService = inject(TextExerciseService); + private textAssessmentService = inject(TextAssessmentService); + private translateService = inject(TranslateService); + tutorEfforts: TutorEffort[] = []; numberOfSubmissions: number; totalTimeSpent: number; @@ -43,13 +49,7 @@ export class TutorEffortStatisticsComponent extends PlagiarismAndTutorEffortDire // Icons faSync = faSync; - constructor( - private textExerciseService: TextExerciseService, - private textAssessmentService: TextAssessmentService, - private route: ActivatedRoute, - private translateService: TranslateService, - private router: Router, - ) { + constructor() { super(); this.translateService.onLangChange.subscribe(() => { this.translateLabels(); diff --git a/src/main/webapp/app/exercises/text/participate/text-editor.component.ts b/src/main/webapp/app/exercises/text/participate/text-editor.component.ts index 94659dfc211d..81deae339df6 100644 --- a/src/main/webapp/app/exercises/text/participate/text-editor.component.ts +++ b/src/main/webapp/app/exercises/text/participate/text-editor.component.ts @@ -1,5 +1,4 @@ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; +import { Component, Input, OnDestroy, OnInit, inject } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { HttpErrorResponse } from '@angular/common/http'; import { AlertService } from 'app/core/util/alert.service'; @@ -34,7 +33,7 @@ import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; import { PROFILE_IRIS } from 'app/app.constants'; import { IrisSettingsService } from 'app/iris/settings/shared/iris-settings.service'; import { AssessmentType } from 'app/entities/assessment-type.model'; -import { CourseExerciseService } from 'app/exercises/shared/course-exercises/course-exercise.service'; + @Component({ selector: 'jhi-text-editor', templateUrl: './text-editor.component.html', @@ -42,6 +41,16 @@ import { CourseExerciseService } from 'app/exercises/shared/course-exercises/cou styleUrls: ['./text-editor.component.scss'], }) export class TextEditorComponent implements OnInit, OnDestroy, ComponentCanDeactivate { + private route = inject(ActivatedRoute); + private textSubmissionService = inject(TextSubmissionService); + private textService = inject(TextEditorService); + private alertService = inject(AlertService); + private participationWebsocketService = inject(ParticipationWebsocketService); + private stringCountService = inject(StringCountService); + private accountService = inject(AccountService); + private profileService = inject(ProfileService); + private irisSettingsService = inject(IrisSettingsService); + readonly ButtonType = ButtonType; readonly MAX_CHARACTER_COUNT = MAX_SUBMISSION_TEXT_LENGTH; protected readonly Result = Result; @@ -64,7 +73,7 @@ export class TextEditorComponent implements OnInit, OnDestroy, ComponentCanDeact resultWithComplaint?: Result; submission: TextSubmission; course?: Course; - isSaving: boolean; + isSaving: boolean = false; private textEditorInput = new Subject(); textEditorInputObservable = this.textEditorInput.asObservable(); private submissionChange = new Subject(); @@ -96,22 +105,6 @@ export class TextEditorComponent implements OnInit, OnDestroy, ComponentCanDeact showHistory: boolean = false; submissionId: number | undefined; - constructor( - private route: ActivatedRoute, - private textSubmissionService: TextSubmissionService, - private textService: TextEditorService, - private alertService: AlertService, - private translateService: TranslateService, - private participationWebsocketService: ParticipationWebsocketService, - private stringCountService: StringCountService, - private accountService: AccountService, - private courseExerciseService: CourseExerciseService, - private profileService: ProfileService, - private irisSettingsService: IrisSettingsService, - ) { - this.isSaving = false; - } - ngOnInit() { if (this.inputValuesArePresent()) { this.setupComponentWithInputValues(); diff --git a/src/main/webapp/app/exercises/text/participate/text-editor.service.ts b/src/main/webapp/app/exercises/text/participate/text-editor.service.ts index cf7d67767a0a..5c87204ace41 100644 --- a/src/main/webapp/app/exercises/text/participate/text-editor.service.ts +++ b/src/main/webapp/app/exercises/text/participate/text-editor.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, tap } from 'rxjs'; import { francAll } from 'franc-min'; @@ -8,7 +8,7 @@ import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service' @Injectable({ providedIn: 'root' }) export class TextEditorService { - constructor(private http: HttpClient) {} + private http = inject(HttpClient); get(participationId: number): Observable { return this.http diff --git a/src/main/webapp/app/exercises/text/participate/text-result/text-result.component.ts b/src/main/webapp/app/exercises/text/participate/text-result/text-result.component.ts index c63aaece1335..00fdd151b73b 100644 --- a/src/main/webapp/app/exercises/text/participate/text-result/text-result.component.ts +++ b/src/main/webapp/app/exercises/text/participate/text-result/text-result.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, inject } from '@angular/core'; import { Feedback, buildFeedbackTextForReview, checkSubsequentFeedbackInAssessment } from 'app/entities/feedback.model'; import { TextSubmission } from 'app/entities/text/text-submission.model'; import { Result } from 'app/entities/result.model'; @@ -15,6 +15,9 @@ import { Course } from 'app/entities/course.model'; styleUrls: ['./text-result.component.scss'], }) export class TextResultComponent { + private translateService = inject(TranslateService); + private localeConversionService = inject(LocaleConversionService); + public submissionText: string; public textResults: TextResultBlock[]; @@ -40,11 +43,6 @@ export class TextResultComponent { @Input() course?: Course; - constructor( - private translateService: TranslateService, - private localeConversionService: LocaleConversionService, - ) {} - private convertTextToResultBlocks(feedbacks: Feedback[] = []): void { checkSubsequentFeedbackInAssessment(feedbacks); diff --git a/src/main/webapp/app/exercises/text/participate/text-submission.service.ts b/src/main/webapp/app/exercises/text/participate/text-submission.service.ts index 2c90d0c7188c..7d406b3fe8d3 100644 --- a/src/main/webapp/app/exercises/text/participate/text-submission.service.ts +++ b/src/main/webapp/app/exercises/text/participate/text-submission.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -12,10 +12,8 @@ export type EntityResponseType = HttpResponse; @Injectable({ providedIn: 'root' }) export class TextSubmissionService { - constructor( - private http: HttpClient, - private submissionService: SubmissionService, - ) {} + private http = inject(HttpClient); + private submissionService = inject(SubmissionService); create(textSubmission: TextSubmission, exerciseId: number): Observable { const copy = this.submissionService.convert(textSubmission); diff --git a/src/main/webapp/app/exercises/text/shared/manual-text-selection/manual-text-selection.component.ts b/src/main/webapp/app/exercises/text/shared/manual-text-selection/manual-text-selection.component.ts index 1a846ba58988..b7c83334e01e 100644 --- a/src/main/webapp/app/exercises/text/shared/manual-text-selection/manual-text-selection.component.ts +++ b/src/main/webapp/app/exercises/text/shared/manual-text-selection/manual-text-selection.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Component, EventEmitter, Input, Output, inject } from '@angular/core'; import { TextAssessmentEventType } from 'app/entities/text/text-assesment-event.model'; import { FeedbackType } from 'app/entities/feedback.model'; import { TextBlockType } from 'app/entities/text/text-block.model'; @@ -21,6 +21,9 @@ const SPACE = ' '; styleUrls: ['./manual-text-selection.component.scss'], }) export class ManualTextSelectionComponent { + protected route = inject(ActivatedRoute); + textAssessmentAnalytics = inject(TextAssessmentAnalytics); + @Input() public textBlockRefGroup: TextBlockRefGroup; @Input() submission: TextSubmission; @Output() public didSelectWord = new EventEmitter(); @@ -33,11 +36,8 @@ export class ManualTextSelectionComponent { public selectedWords = new Array(); public ready = false; - constructor( - public textAssessmentAnalytics: TextAssessmentAnalytics, - protected route: ActivatedRoute, - ) { - textAssessmentAnalytics.setComponentRoute(route); + constructor() { + this.textAssessmentAnalytics.setComponentRoute(this.route); } calculateIndex(index: number): void { diff --git a/src/main/webapp/app/exercises/text/shared/text-select.directive.ts b/src/main/webapp/app/exercises/text/shared/text-select.directive.ts index 3ff564df7462..8ead1a2d74ee 100644 --- a/src/main/webapp/app/exercises/text/shared/text-select.directive.ts +++ b/src/main/webapp/app/exercises/text/shared/text-select.directive.ts @@ -1,4 +1,4 @@ -import { Directive, ElementRef, EventEmitter, NgZone, OnDestroy, OnInit, Output } from '@angular/core'; +import { Directive, ElementRef, EventEmitter, NgZone, OnDestroy, OnInit, Output, inject } from '@angular/core'; /** * Disclaimer: @@ -32,15 +32,13 @@ enum EventType { selector: '[jhiTextSelect]', }) export class TextSelectDirective implements OnInit, OnDestroy { + private elementRef = inject(ElementRef); + private zone = inject(NgZone); + @Output() public jhiTextSelect = new EventEmitter(); private hasSelection = false; - constructor( - private elementRef: ElementRef, - private zone: NgZone, - ) {} - /** * Init text select directive by adding event listenes mouseDown and selectionChange event listeners to element. */ diff --git a/src/main/webapp/app/iris/exercise-chatbot/widget/chatbot-widget.component.html b/src/main/webapp/app/iris/exercise-chatbot/widget/chatbot-widget.component.html index 427b8bc2184a..9b19aeec3384 100644 --- a/src/main/webapp/app/iris/exercise-chatbot/widget/chatbot-widget.component.html +++ b/src/main/webapp/app/iris/exercise-chatbot/widget/chatbot-widget.component.html @@ -2,5 +2,6 @@
+
diff --git a/src/main/webapp/app/iris/exercise-chatbot/widget/chatbot-widget.component.scss b/src/main/webapp/app/iris/exercise-chatbot/widget/chatbot-widget.component.scss index 23ff98fcc30b..e27dec60427a 100644 --- a/src/main/webapp/app/iris/exercise-chatbot/widget/chatbot-widget.component.scss +++ b/src/main/webapp/app/iris/exercise-chatbot/widget/chatbot-widget.component.scss @@ -15,6 +15,13 @@ flex-direction: column; } +.chat-widget-top-resize-area { + position: absolute; + height: 5px; + width: 100%; + z-index: 10; +} + .ng-draggable { cursor: grab; } diff --git a/src/main/webapp/app/iris/exercise-chatbot/widget/chatbot-widget.component.ts b/src/main/webapp/app/iris/exercise-chatbot/widget/chatbot-widget.component.ts index be84a8d62c97..11d27fdd8d12 100644 --- a/src/main/webapp/app/iris/exercise-chatbot/widget/chatbot-widget.component.ts +++ b/src/main/webapp/app/iris/exercise-chatbot/widget/chatbot-widget.component.ts @@ -44,7 +44,7 @@ export class IrisChatbotWidgetComponent implements OnDestroy, AfterViewInit { interact('.chat-widget') .resizable({ // resize from all edges and corners - edges: { left: true, right: true, bottom: true, top: true }, + edges: { left: true, right: true, bottom: true, top: '.chat-widget-top-resize-area' }, listeners: { move: (event) => { @@ -85,6 +85,7 @@ export class IrisChatbotWidgetComponent implements OnDestroy, AfterViewInit { inertia: true, }) .draggable({ + allowFrom: '.chat-header', listeners: { move: (event: any) => { const target = event.target, diff --git a/src/main/webapp/app/lecture/lecture-attachments.component.html b/src/main/webapp/app/lecture/lecture-attachments.component.html index c20d21a173c5..6192f270d64e 100644 --- a/src/main/webapp/app/lecture/lecture-attachments.component.html +++ b/src/main/webapp/app/lecture/lecture-attachments.component.html @@ -1,29 +1,29 @@
- @if (lecture) { + @if (lecture()) {
- @if (showHeader) { + @if (showHeader()) {
-

: {{ lecture.title }} - {{ lecture.course?.shortName }}

+

: {{ lecture().title }} - {{ lecture().course?.shortName }}


- {{ lecture.startDate | artemisDate }} + {{ lecture().startDate | artemisDate }}
- {{ lecture.endDate | artemisDate }} + {{ lecture().endDate | artemisDate }}
-
+

@@ -83,7 +83,7 @@

- @if (lecture.isAtLeastInstructor) { + @if (lecture().isAtLeastInstructor) {

@if (!attachmentToBeCreated.id) {

- } - @if (attachmentToBeCreated.id) { + } @else {

}
@@ -198,11 +198,11 @@

- -
@@ -212,7 +212,7 @@

@if (!attachmentToBeCreated) {
-
diff --git a/src/main/webapp/app/lecture/lecture-attachments.component.ts b/src/main/webapp/app/lecture/lecture-attachments.component.ts index 6bb35a0e84af..262bbfe593a5 100644 --- a/src/main/webapp/app/lecture/lecture-attachments.component.ts +++ b/src/main/webapp/app/lecture/lecture-attachments.component.ts @@ -1,9 +1,9 @@ -import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { Component, ElementRef, OnDestroy, ViewChild, effect, inject, input, signal } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { Lecture } from 'app/entities/lecture.model'; import dayjs from 'dayjs/esm'; -import { Subject } from 'rxjs'; +import { Subject, Subscription } from 'rxjs'; import { FileService } from 'app/shared/http/file.service'; import { Attachment, AttachmentType } from 'app/entities/attachment.model'; import { AttachmentService } from 'app/lecture/attachment.service'; @@ -16,7 +16,7 @@ import { LectureService } from 'app/lecture/lecture.service'; templateUrl: './lecture-attachments.component.html', styleUrls: ['./lecture-attachments.component.scss'], }) -export class LectureAttachmentsComponent implements OnInit, OnDestroy { +export class LectureAttachmentsComponent implements OnDestroy { protected readonly faSpinner = faSpinner; protected readonly faTimes = faTimes; protected readonly faTrash = faTrash; @@ -28,11 +28,16 @@ export class LectureAttachmentsComponent implements OnInit, OnDestroy { protected readonly allowedFileExtensions = ALLOWED_FILE_EXTENSIONS_HUMAN_READABLE; protected readonly acceptedFileExtensionsFileBrowser = ACCEPTED_FILE_EXTENSIONS_FILE_BROWSER; + private readonly activatedRoute = inject(ActivatedRoute); + private readonly attachmentService = inject(AttachmentService); + private readonly lectureService = inject(LectureService); + private readonly fileService = inject(FileService); + @ViewChild('fileInput', { static: false }) fileInput: ElementRef; - @Input() lectureId: number | undefined; - @Input() showHeader = true; + lectureId = input(); + showHeader = input(true); - lecture: Lecture; + lecture = signal(new Lecture()); attachments: Attachment[] = []; attachmentToBeCreated?: Attachment; attachmentBackup?: Attachment; @@ -46,30 +51,31 @@ export class LectureAttachmentsComponent implements OnInit, OnDestroy { private dialogErrorSource = new Subject(); dialogError$ = this.dialogErrorSource.asObservable(); - constructor( - protected activatedRoute: ActivatedRoute, - private attachmentService: AttachmentService, - private lectureService: LectureService, - private fileService: FileService, - ) {} - - ngOnInit() { - this.notificationText = undefined; - this.activatedRoute.parent!.data.subscribe(({ lecture }) => { - if (this.lectureId) { - this.lectureService.findWithDetails(this.lectureId).subscribe((lectureResponse: HttpResponse) => { - this.lecture = lectureResponse.body!; - this.loadAttachments(); + private routeDataSubscription?: Subscription; + + constructor() { + effect( + () => { + this.notificationText = undefined; + this.routeDataSubscription?.unsubscribe(); // in case the subscription was already defined + this.routeDataSubscription = this.activatedRoute.parent!.data.subscribe(({ lecture }) => { + if (this.lectureId()) { + this.lectureService.findWithDetails(this.lectureId()!).subscribe((lectureResponse: HttpResponse) => { + this.lecture.set(lectureResponse.body!); + this.loadAttachments(); + }); + } else { + this.lecture.set(lecture); + this.loadAttachments(); + } }); - } else { - this.lecture = lecture; - this.loadAttachments(); - } - }); + }, + { allowSignalWrites: true }, + ); } loadAttachments(): void { - this.attachmentService.findAllByLectureId(this.lecture.id!).subscribe((attachmentsResponse: HttpResponse) => { + this.attachmentService.findAllByLectureId(this.lecture().id!).subscribe((attachmentsResponse: HttpResponse) => { this.attachments = attachmentsResponse.body!; this.attachments.forEach((attachment) => { this.viewButtonAvailable[attachment.id!] = this.isViewButtonAvailable(attachment.link!); @@ -79,6 +85,7 @@ export class LectureAttachmentsComponent implements OnInit, OnDestroy { ngOnDestroy(): void { this.dialogErrorSource.unsubscribe(); + this.routeDataSubscription?.unsubscribe(); } isViewButtonAvailable(attachmentLink: string): boolean { @@ -91,7 +98,7 @@ export class LectureAttachmentsComponent implements OnInit, OnDestroy { addAttachment(): void { const newAttachment = new Attachment(); - newAttachment.lecture = this.lecture; + newAttachment.lecture = this.lecture(); newAttachment.attachmentType = AttachmentType.FILE; newAttachment.version = 0; newAttachment.uploadDate = dayjs(); @@ -133,8 +140,8 @@ export class LectureAttachmentsComponent implements OnInit, OnDestroy { this.attachmentService.create(this.attachmentToBeCreated!, this.attachmentFile!).subscribe({ next: (attachmentRes: HttpResponse) => { this.attachments.push(attachmentRes.body!); - this.lectureService.findWithDetails(this.lecture.id!).subscribe((lectureResponse: HttpResponse) => { - this.lecture = lectureResponse.body!; + this.lectureService.findWithDetails(this.lecture().id!).subscribe((lectureResponse: HttpResponse) => { + this.lecture.set(lectureResponse.body!); }); this.attachmentFile = undefined; this.attachmentToBeCreated = undefined; diff --git a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/attachment-unit-form/attachment-unit-form.component.ts b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/attachment-unit-form/attachment-unit-form.component.ts index 6ed702b59547..1bc42524d04b 100644 --- a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/attachment-unit-form/attachment-unit-form.component.ts +++ b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/attachment-unit-form/attachment-unit-form.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, OnChanges, ViewChild, computed, inject, input, output, signal } from '@angular/core'; +import { Component, ElementRef, OnChanges, ViewChild, computed, inject, input, output, signal, viewChild } from '@angular/core'; import dayjs from 'dayjs/esm'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { faQuestionCircle, faTimes } from '@fortawesome/free-solid-svg-icons'; @@ -6,6 +6,7 @@ import { ACCEPTED_FILE_EXTENSIONS_FILE_BROWSER, ALLOWED_FILE_EXTENSIONS_HUMAN_RE import { CompetencyLectureUnitLink } from 'app/entities/competency.model'; import { MAX_FILE_SIZE } from 'app/shared/constants/input.constants'; import { toSignal } from '@angular/core/rxjs-interop'; +import { FormDateTimePickerComponent } from 'app/shared/date-time-picker/date-time-picker.component'; export interface AttachmentUnitFormData { formProperties: FormProperties; @@ -47,6 +48,8 @@ export class AttachmentUnitFormComponent implements OnChanges { hasCancelButton = input(false); onCancel = output(); + datePickerComponent = viewChild(FormDateTimePickerComponent); + // have to handle the file input as a special case at is not part of the reactive form @ViewChild('fileInput', { static: false }) fileInput: ElementRef; @@ -68,7 +71,7 @@ export class AttachmentUnitFormComponent implements OnChanges { private readonly statusChanges = toSignal(this.form.statusChanges ?? 'INVALID'); isFormValid = computed(() => { - return (this.statusChanges() === 'VALID' || this.fileName()) && !this.isFileTooBig(); + return (this.statusChanges() === 'VALID' || this.fileName()) && !this.isFileTooBig() && this.datePickerComponent()?.isValid(); }); ngOnChanges(): void { diff --git a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/online-unit-form/online-unit-form.component.ts b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/online-unit-form/online-unit-form.component.ts index 67adf4f2c8dc..241feda894e8 100644 --- a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/online-unit-form/online-unit-form.component.ts +++ b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/online-unit-form/online-unit-form.component.ts @@ -1,5 +1,5 @@ import dayjs from 'dayjs/esm'; -import { Component, OnChanges, computed, inject, input, output } from '@angular/core'; +import { Component, OnChanges, computed, inject, input, output, viewChild } from '@angular/core'; import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { faArrowLeft, faTimes } from '@fortawesome/free-solid-svg-icons'; import { map } from 'rxjs'; @@ -8,6 +8,7 @@ import { OnlineResourceDTO } from 'app/lecture/lecture-unit/lecture-unit-managem import { OnlineUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/onlineUnit.service'; import { CompetencyLectureUnitLink } from 'app/entities/competency.model'; import { toSignal } from '@angular/core/rxjs-interop'; +import { FormDateTimePickerComponent } from 'app/shared/date-time-picker/date-time-picker.component'; export interface OnlineUnitFormData { name?: string; @@ -45,6 +46,8 @@ export class OnlineUnitFormComponent implements OnChanges { hasCancelButton = input(false); onCancel = output(); + datePickerComponent = viewChild(FormDateTimePickerComponent); + urlValidator = urlValidator; private readonly formBuilder = inject(FormBuilder); @@ -59,7 +62,7 @@ export class OnlineUnitFormComponent implements OnChanges { }); private readonly statusChanges = toSignal(this.form.statusChanges ?? 'INVALID'); - isFormValid = computed(() => this.statusChanges() === 'VALID'); + isFormValid = computed(() => this.statusChanges() === 'VALID' && this.datePickerComponent()?.isValid()); get nameControl() { return this.form.get('name'); diff --git a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/text-unit-form/text-unit-form.component.ts b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/text-unit-form/text-unit-form.component.ts index b6d7b31447b2..3ed05d37b8c1 100644 --- a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/text-unit-form/text-unit-form.component.ts +++ b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/text-unit-form/text-unit-form.component.ts @@ -1,4 +1,4 @@ -import { Component, OnChanges, OnDestroy, OnInit, computed, inject, input, output } from '@angular/core'; +import { Component, OnChanges, OnDestroy, OnInit, computed, inject, input, output, viewChild } from '@angular/core'; import dayjs from 'dayjs/esm'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; @@ -8,6 +8,7 @@ import { TranslateService } from '@ngx-translate/core'; import { faTimes } from '@fortawesome/free-solid-svg-icons'; import { CompetencyLectureUnitLink } from 'app/entities/competency.model'; import { toSignal } from '@angular/core/rxjs-interop'; +import { FormDateTimePickerComponent } from 'app/shared/date-time-picker/date-time-picker.component'; export interface TextUnitFormData { name?: string; @@ -32,6 +33,8 @@ export class TextUnitFormComponent implements OnInit, OnChanges, OnDestroy { hasCancelButton = input(false); onCancel = output(); + datePickerComponent = viewChild(FormDateTimePickerComponent); + // not included in reactive form content: string | undefined; contentLoadedFromCache = false; @@ -46,7 +49,7 @@ export class TextUnitFormComponent implements OnInit, OnChanges, OnDestroy { }); private readonly statusChanges = toSignal(this.form.statusChanges ?? 'INVALID'); - isFormValid = computed(() => this.statusChanges() === 'VALID'); + isFormValid = computed(() => this.statusChanges() === 'VALID' && this.datePickerComponent()?.isValid()); private markdownChanges = new Subject(); private markdownChangesSubscription: Subscription; diff --git a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/video-unit-form/video-unit-form.component.ts b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/video-unit-form/video-unit-form.component.ts index 543526c9491b..0ffea72952dc 100644 --- a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/video-unit-form/video-unit-form.component.ts +++ b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/video-unit-form/video-unit-form.component.ts @@ -1,10 +1,11 @@ import dayjs from 'dayjs/esm'; -import { Component, computed, effect, inject, input, output, untracked } from '@angular/core'; +import { Component, computed, effect, inject, input, output, untracked, viewChild } from '@angular/core'; import { AbstractControl, FormBuilder, FormGroup, ValidationErrors, Validators } from '@angular/forms'; import urlParser from 'js-video-url-parser'; import { faArrowLeft, faTimes } from '@fortawesome/free-solid-svg-icons'; import { CompetencyLectureUnitLink } from 'app/entities/competency.model'; import { toSignal } from '@angular/core/rxjs-interop'; +import { FormDateTimePickerComponent } from 'app/shared/date-time-picker/date-time-picker.component'; export interface VideoUnitFormData { name?: string; @@ -72,6 +73,8 @@ export class VideoUnitFormComponent { hasCancelButton = input(); onCancel = output(); + datePickerComponent = viewChild(FormDateTimePickerComponent); + videoSourceUrlValidator = videoSourceUrlValidator; videoSourceTransformUrlValidator = videoSourceTransformUrlValidator; @@ -84,7 +87,7 @@ export class VideoUnitFormComponent { competencyLinks: [undefined as CompetencyLectureUnitLink[] | undefined], }); private readonly statusChanges = toSignal(this.form.statusChanges ?? 'INVALID'); - isFormValid = computed(() => this.statusChanges() === 'VALID'); + isFormValid = computed(() => this.statusChanges() === 'VALID' && this.datePickerComponent()?.isValid()); constructor() { effect(() => { diff --git a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-period.component.ts b/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-period.component.ts index 8953b9e67deb..7d9b48571621 100644 --- a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-period.component.ts +++ b/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-period.component.ts @@ -9,6 +9,4 @@ export class LectureUpdateWizardPeriodComponent { @Input() currentStep: number; @Input() lecture: Lecture; @Input() validateDatesFunction: () => void; - - constructor() {} } diff --git a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-title.component.ts b/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-title.component.ts index 2c456af3f0e7..e354df72e629 100644 --- a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-title.component.ts +++ b/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-title.component.ts @@ -11,6 +11,4 @@ export class LectureUpdateWizardTitleComponent { @Input() lecture: Lecture; domainActionsDescription = [new FormulaAction()]; - - constructor() {} } diff --git a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-units.component.html b/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-units.component.html index 9d838c196f05..3736d642d80e 100644 --- a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-units.component.html +++ b/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-units.component.html @@ -1,5 +1,5 @@
-

+

- } - @if (isAnyUnitFormOpen()) { + } @else {
@if (!isEditingLectureUnit) {

- } - @if (isEditingLectureUnit) { + } @else {

} - @if (isTextUnitFormOpen) { + @if (isTextUnitFormOpen()) { } - @if (isVideoUnitFormOpen) { + @if (isVideoUnitFormOpen()) { } - @if (isOnlineUnitFormOpen) { + @if (isOnlineUnitFormOpen()) { } - @if (isAttachmentUnitFormOpen) { + @if (isAttachmentUnitFormOpen()) { } - @if (isExerciseUnitFormOpen) { + @if (isExerciseUnitFormOpen()) { (false); + isExerciseUnitFormOpen = signal(false); + isVideoUnitFormOpen = signal(false); + isOnlineUnitFormOpen = signal(false); + isAttachmentUnitFormOpen = signal(false); currentlyProcessedTextUnit: TextUnit; currentlyProcessedVideoUnit: VideoUnit; @@ -71,33 +71,33 @@ export class LectureUpdateWizardUnitsComponent implements OnInit { switch (type) { case LectureUnitType.TEXT: - this.isTextUnitFormOpen = true; + this.isTextUnitFormOpen.set(true); break; case LectureUnitType.EXERCISE: - this.isExerciseUnitFormOpen = true; + this.isExerciseUnitFormOpen.set(true); break; case LectureUnitType.VIDEO: - this.isVideoUnitFormOpen = true; + this.isVideoUnitFormOpen.set(true); break; case LectureUnitType.ONLINE: - this.isOnlineUnitFormOpen = true; + this.isOnlineUnitFormOpen.set(true); break; case LectureUnitType.ATTACHMENT: - this.isAttachmentUnitFormOpen = true; + this.isAttachmentUnitFormOpen.set(true); break; } } - isAnyUnitFormOpen(): boolean { - return this.isTextUnitFormOpen || this.isVideoUnitFormOpen || this.isOnlineUnitFormOpen || this.isAttachmentUnitFormOpen || this.isExerciseUnitFormOpen; - } + isAnyUnitFormOpen = computed(() => { + return this.isTextUnitFormOpen() || this.isVideoUnitFormOpen() || this.isOnlineUnitFormOpen() || this.isAttachmentUnitFormOpen() || this.isExerciseUnitFormOpen(); + }); onCloseLectureUnitForms() { - this.isTextUnitFormOpen = false; - this.isVideoUnitFormOpen = false; - this.isOnlineUnitFormOpen = false; - this.isAttachmentUnitFormOpen = false; - this.isExerciseUnitFormOpen = false; + this.isTextUnitFormOpen.set(false); + this.isVideoUnitFormOpen.set(false); + this.isOnlineUnitFormOpen.set(false); + this.isAttachmentUnitFormOpen.set(false); + this.isExerciseUnitFormOpen.set(false); } createEditTextUnit(formData: TextUnitFormData) { @@ -258,11 +258,11 @@ export class LectureUpdateWizardUnitsComponent implements OnInit { this.currentlyProcessedOnlineUnit = lectureUnit as OnlineUnit; this.currentlyProcessedAttachmentUnit = lectureUnit as AttachmentUnit; - this.isTextUnitFormOpen = lectureUnit.type === LectureUnitType.TEXT; - this.isVideoUnitFormOpen = lectureUnit.type === LectureUnitType.VIDEO; - this.isExerciseUnitFormOpen = lectureUnit.type === LectureUnitType.EXERCISE; - this.isOnlineUnitFormOpen = lectureUnit.type === LectureUnitType.ONLINE; - this.isAttachmentUnitFormOpen = lectureUnit.type === LectureUnitType.ATTACHMENT; + this.isTextUnitFormOpen.set(lectureUnit.type === LectureUnitType.TEXT); + this.isVideoUnitFormOpen.set(lectureUnit.type === LectureUnitType.VIDEO); + this.isExerciseUnitFormOpen.set(lectureUnit.type === LectureUnitType.EXERCISE); + this.isOnlineUnitFormOpen.set(lectureUnit.type === LectureUnitType.ONLINE); + this.isAttachmentUnitFormOpen.set(lectureUnit.type === LectureUnitType.ATTACHMENT); switch (lectureUnit.type) { case LectureUnitType.TEXT: diff --git a/src/main/webapp/app/shared/metis/answer-post/answer-post.component.html b/src/main/webapp/app/shared/metis/answer-post/answer-post.component.html index 073e4e907593..1579e97f9a0f 100644 --- a/src/main/webapp/app/shared/metis/answer-post/answer-post.component.html +++ b/src/main/webapp/app/shared/metis/answer-post/answer-post.component.html @@ -5,7 +5,6 @@ [isReadOnlyMode]="isReadOnlyMode" [isCommunicationPage]="isCommunicationPage" [lastReadDate]="lastReadDate" - [hasChannelModerationRights]="hasChannelModerationRights" [isDeleted]="isDeleted" /> } @@ -32,7 +31,8 @@ [isThreadSidebar]="isThreadSidebar" (openPostingCreateEditModal)="createAnswerPostModal.open()" (reactionsUpdated)="onReactionsUpdated($event)" - (mayEditOrDeleteOutput)="onMayEditOrDelete($event)" + (mayDeleteOutput)="onMayDelete($event)" + (mayEditOutput)="onMayEdit($event)" (isDeleteEvent)="onDeleteEvent(true)" />
@@ -64,11 +64,13 @@ - @if (mayEditOrDelete) { + @if (mayEdit) { + } + @if (mayDelete) { } - @if (mayEditOrDelete) { + @if (mayEdit) { + } + @if (mayDelete) { } - diff --git a/src/main/webapp/app/shared/metis/post/post.component.scss b/src/main/webapp/app/shared/metis/post/post.component.scss index 1a6cf4b3ace2..10cb7ef83132 100644 --- a/src/main/webapp/app/shared/metis/post/post.component.scss +++ b/src/main/webapp/app/shared/metis/post/post.component.scss @@ -20,6 +20,7 @@ top: -1.8rem; right: 1%; display: flex; + max-height: 2.2rem; gap: 10px; visibility: hidden; transition: diff --git a/src/main/webapp/app/shared/metis/post/post.component.ts b/src/main/webapp/app/shared/metis/post/post.component.ts index d70c13abae68..7bfe1d0c6f49 100644 --- a/src/main/webapp/app/shared/metis/post/post.component.ts +++ b/src/main/webapp/app/shared/metis/post/post.component.ts @@ -77,7 +77,8 @@ export class PostComponent extends PostingDirective implements OnInit, OnC contextInformation: ContextInformation; readonly PageType = PageType; readonly DisplayPriority = DisplayPriority; - mayEditOrDelete: boolean = false; + mayEdit: boolean = false; + mayDelete: boolean = false; canPin: boolean = false; // Icons @@ -90,7 +91,7 @@ export class PostComponent extends PostingDirective implements OnInit, OnC isConsecutive = input(false); dropdownPosition = { x: 0, y: 0 }; - @ViewChild(PostReactionsBarComponent) private reactionsBarComponent!: PostReactionsBarComponent; + @ViewChild(PostReactionsBarComponent) protected reactionsBarComponent!: PostReactionsBarComponent; constructor( public metisService: MetisService, @@ -112,8 +113,12 @@ export class PostComponent extends PostingDirective implements OnInit, OnC return this.posting.displayPriority === DisplayPriority.PINNED; } - onMayEditOrDelete(value: boolean) { - this.mayEditOrDelete = value; + onMayEdit(value: boolean) { + this.mayEdit = value; + } + + onMayDelete(value: boolean) { + this.mayDelete = value; } onCanPin(value: boolean) { @@ -218,6 +223,13 @@ export class PostComponent extends PostingDirective implements OnInit, OnC this.postFooterComponent.openCreateAnswerPostModal(); } + /** + * Close create answer modal + */ + closeCreateAnswerPostModal() { + this.postFooterComponent.closeCreateAnswerPostModal(); + } + /** * sorts answerPosts by two criteria * 1. criterion: resolvesPost -> true comes first diff --git a/src/main/webapp/app/shared/metis/posting-footer/post-footer/post-footer.component.ts b/src/main/webapp/app/shared/metis/posting-footer/post-footer/post-footer.component.ts index 7f6d3b0531a5..5808a9b342ba 100644 --- a/src/main/webapp/app/shared/metis/posting-footer/post-footer/post-footer.component.ts +++ b/src/main/webapp/app/shared/metis/posting-footer/post-footer/post-footer.component.ts @@ -173,6 +173,13 @@ export class PostFooterComponent extends PostingFooterDirective implements this.createAnswerPostModalComponent.open(); } + /** + * Close create answer modal + */ + closeCreateAnswerPostModal() { + this.createAnswerPostModalComponent.close(); + } + protected postsTrackByFn(index: number, post: Post): number { return post.id!; } diff --git a/src/main/webapp/app/shared/metis/posting-header/answer-post-header/answer-post-header.component.ts b/src/main/webapp/app/shared/metis/posting-header/answer-post-header/answer-post-header.component.ts index 9871b3bbee8f..bb1ff3cbbbd2 100644 --- a/src/main/webapp/app/shared/metis/posting-header/answer-post-header/answer-post-header.component.ts +++ b/src/main/webapp/app/shared/metis/posting-header/answer-post-header/answer-post-header.component.ts @@ -2,7 +2,7 @@ import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angu import { AnswerPost } from 'app/entities/metis/answer-post.model'; import { PostingHeaderDirective } from 'app/shared/metis/posting-header/posting-header.directive'; import { MetisService } from 'app/shared/metis/metis.service'; -import { faCheck, faCog, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; +import { faCheck, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; import dayjs from 'dayjs/esm'; import { AccountService } from 'app/core/auth/account.service'; @@ -18,13 +18,9 @@ export class AnswerPostHeaderComponent extends PostingHeaderDirective(); - isAuthorOfOriginalPost: boolean; - isAnswerOfAnnouncement: boolean; - // Icons - faCheck = faCheck; - faPencilAlt = faPencilAlt; - faCog = faCog; + readonly faCheck = faCheck; + readonly faPencilAlt = faPencilAlt; constructor( protected metisService: MetisService, diff --git a/src/main/webapp/app/shared/metis/posting-header/post-header/post-header.component.ts b/src/main/webapp/app/shared/metis/posting-header/post-header/post-header.component.ts index 3463b66456fc..fe4bc129addd 100644 --- a/src/main/webapp/app/shared/metis/posting-header/post-header/post-header.component.ts +++ b/src/main/webapp/app/shared/metis/posting-header/post-header/post-header.component.ts @@ -3,7 +3,7 @@ import { Post } from 'app/entities/metis/post.model'; import { PostingHeaderDirective } from 'app/shared/metis/posting-header/posting-header.directive'; import { MetisService } from 'app/shared/metis/metis.service'; import { PostCreateEditModalComponent } from 'app/shared/metis/posting-create-edit-modal/post-create-edit-modal/post-create-edit-modal.component'; -import { faCheckSquare, faCog, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; +import { faCheckSquare, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; import dayjs from 'dayjs/esm'; import { CachingStrategy } from 'app/shared/image/secured-image.component'; import { AccountService } from 'app/core/auth/account.service'; @@ -22,9 +22,8 @@ export class PostHeaderComponent extends PostingHeaderDirective implements isAtLeastInstructorInCourse: boolean; // Icons - faPencilAlt = faPencilAlt; - faCheckSquare = faCheckSquare; - faCog = faCog; + readonly faPencilAlt = faPencilAlt; + readonly faCheckSquare = faCheckSquare; constructor( protected metisService: MetisService, diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.html b/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.html index 86a4a499a599..1d0c14bff56f 100644 --- a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.html +++ b/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.html @@ -73,11 +73,12 @@ } } - @if (mayEditOrDelete) { + @if (mayEdit) { - + } + @if (mayDelete) { -
- }
diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts b/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts index 9023089670d5..c287db1b1d7b 100644 --- a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts +++ b/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, OnInit, Output, output } from '@angular/core'; import { Reaction } from 'app/entities/metis/reaction.model'; import { PostingsReactionsBarDirective } from 'app/shared/metis/posting-reactions-bar/posting-reactions-bar.directive'; import { AnswerPost } from 'app/entities/metis/answer-post.model'; @@ -22,8 +22,10 @@ export class AnswerPostReactionsBarComponent extends PostingsReactionsBarDirecti @Output() openPostingCreateEditModal = new EventEmitter(); isAuthorOfOriginalPost: boolean; isAnswerOfAnnouncement: boolean; - @Output() mayEditOrDeleteOutput = new EventEmitter(); - mayEditOrDelete: boolean; + mayDeleteOutput = output(); + mayDelete: boolean; + mayEditOutput = output(); + mayEdit: boolean; readonly faPencilAlt = faPencilAlt; @Input() isEmojiCount: boolean = false; @Output() postingUpdated = new EventEmitter(); @@ -34,12 +36,14 @@ export class AnswerPostReactionsBarComponent extends PostingsReactionsBarDirecti ngOnInit() { super.ngOnInit(); - this.setMayEditOrDelete(); + this.setMayEdit(); + this.setMayDelete(); } ngOnChanges() { super.ngOnChanges(); - this.setMayEditOrDelete(); + this.setMayEdit(); + this.setMayDelete(); } isAnyReactionCountAboveZero(): boolean { @@ -64,7 +68,7 @@ export class AnswerPostReactionsBarComponent extends PostingsReactionsBarDirecti return reaction; } - setMayEditOrDelete(): void { + setMayDelete(): void { // determines if the current user is the author of the original post, that the answer belongs to this.isAuthorOfOriginalPost = this.metisService.metisUserIsAuthorOfPosting(this.posting.post!); this.isAnswerOfAnnouncement = getAsChannelDTO(this.posting.post?.conversation)?.isAnnouncementChannel ?? false; @@ -72,8 +76,13 @@ export class AnswerPostReactionsBarComponent extends PostingsReactionsBarDirecti const isAtLeastInstructorInCourse = this.metisService.metisUserIsAtLeastInstructorInCourse(); const mayEditOrDeleteOtherUsersAnswer = (isCourseWideChannel && isAtLeastInstructorInCourse) || (getAsChannelDTO(this.metisService.getCurrentConversation())?.hasChannelModerationRights ?? false); - this.mayEditOrDelete = !this.isReadOnlyMode && (this.isAuthorOfPosting || mayEditOrDeleteOtherUsersAnswer); - this.mayEditOrDeleteOutput.emit(this.mayEditOrDelete); + this.mayDelete = !this.isReadOnlyMode && (this.isAuthorOfPosting || mayEditOrDeleteOtherUsersAnswer); + this.mayDeleteOutput.emit(this.mayDelete); + } + + setMayEdit() { + this.mayEdit = this.isAuthorOfPosting; + this.mayEditOutput.emit(this.mayEdit); } editPosting() { diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.html b/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.html index 5ba32fbe573e..bf818ff12160 100644 --- a/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.html +++ b/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.html @@ -1,10 +1,18 @@ -
+
+ @if (hoverBar && sortedAnswerPosts.length === 0) { +
+ +
+ } @if (!isCommunicationPage) { @if (sortedAnswerPosts.length) { @if (showAnswers) {
- @@ -12,8 +20,8 @@ } @else {
-
} - } @else { - -
- -
} } @else { @if (!isThreadSidebar) { - - @if (hoverBar && sortedAnswerPosts.length === 0) { -
- -
- } @if (!showAnswers && sortedAnswerPosts.length) {
@@ -108,7 +99,7 @@ } - @if (!isEmojiCount && mayEditOrDelete) { + @if (!isEmojiCount && mayEdit) {